2.9 ソフトウェア構築
概要と動機
ソフトウェア構築は、設計が動くコードになる場所です。コーディング、検証、ユニットテスト、統合テスト、デバッグという詳細な仕事です。ソフトウェアエンジニアリング知識体系(SWEBOK)ガイドは、構築を独立した知識領域として扱っており、それには理由があります。ここで日々の仕事の大半が行われるからです。行ごとに行う選択(複雑さをどう封じ込めるか、エラーをどう扱うか、どれだけ読みやすくしておくか)が、システムが何年にもわたって理解され、変更され、信頼されるかどうかを決めます。
大きなチームでは、構築は個人の仕事ではなく、集団の仕事です。何百人ものエンジニアが、誰か一人のチームでの在任期間より長生きする共有のコードベースに書き込みます。だから基準は「今日、私のマシンで動くか」ではありません。「五年後に、見知らぬ人が安全にこれを変更できるか」です。構築は上方向で、何を作るか、その形を教える要求(2.8章)と設計(2.2章)につながります。横方向では、作業がどう表現され、検証され、検査されるかを形づくるコーディング標準(2.1章)、テスト(2.4章)、コードレビュー(2.5章)につながります。良い構築は、堅実な設計を保守可能な資産に変えます。まずい構築は、良い設計さえ負債に変えます。
企業や政府の設定では、構築には特別な重みがあります。これらのシステムは長寿命で、厳しく規制され、しばしば安全や市民に関わる重要なものです。防御的コーディング、規律あるエラー処理、一目で正しいとわかるコードは、ここでは気の利いたものではなく、数十年と人員の入れ替わりを越える、保証、監査、継続性のための要件です。目標は、意図を伝え、失敗に抵抗し、検証できるコードです。単に動くだけのコードでは足りません。
主要原則
- 何よりも複雑さを最小化します。大規模な構築の主な敵は、誰も完全には理解できないコードです。
- 変更を予期します。将来ありそうな変更が局所的で安くなるように構築します。
- 検証のために構築します。テスト、レビュー、推論で正しさを確かめやすいコードを書きます。
- 意図して再利用します。再発明するのではなく信頼できる既存のコンポーネントの上に築きますが、誤った抽象化への結合は避けます。
- 標準に従います。コードベース全体の一貫性は、将来のあらゆる変更の認知的コストを下げます。
- エラーと不正な状態を明示的に扱います。失敗モードを黙らせず、見えるようにします。
- コードを読みやすく保ちます。構築は、まず将来の保守担当者との、次にコンパイラとのコミュニケーションです。
推奨事項
主要な規律として複雑さを最小化する
本質的なものも偶有的なものも、複雑さを減らすことを中心的な目標にします。小さく、単一目的の関数とモジュールを書きます。巧妙なトリックより明確な名前を好みます。ネストを浅く、制御フローを直線的に保ちます。決定を局所化し、一つのコードを理解するのにシステム全体を頭に保たなくて済むようにします。複雑さは、大きなコードベースを変更に遅く、触れるのに危険にするものなので、あらゆる選択を、複雑さを加えるか取り除くかで量ります。2.2章の設計原則を小さな規模にも適用します。高凝集、低結合、明確な関心の分離は、アーキテクチャと同じくらい、単一の関数でも重要です。
変更のために、そして検証のために構築する
来る可能性が最も高い変更(新しいビジネスルール、新しい統合、新しい規制)を先に見越し、変更が局所に留まるよう、安定したインターフェースの背後に隔離します。同時に、検証しやすいコードを書きます。できる所では純粋関数(同じ入力が常に同じ出力を生み、副作用がない)、隠れた状態を最小限に、そしてテストが置き換えられるよう依存関係を明示的に。テストしにくいコードは、たいてい理解しにくく、変更しにくいコードです。テスト容易性(2.4章)は、QAだけの関心事ではなく、設計のシグナルです。
意図して再利用し、標準化する
基礎的なロジックを書き直す前に、よく保守された信頼できるライブラリや社内コンポーネントに手を伸ばし、明確なインターフェース(2.3章)を通して使います。再利用可能なコンポーネントは、本物の二つ目のユースケースがあるときにだけ作ります。早すぎる一般化は、それ自体が複雑さの一形態だからです。組織のコーディング標準とスタイル(2.1章)を、理想的には自動のフォーマッターとリンターで徹底して一様に適用し、コードベース全体が、一人の注意深い作者が書いたかのように読めるようにします。
判断をもって防御的プログラミングを実践する
信頼の境界(外部のリクエスト、ファイルとネットワークのI/O、ユーザー入力)で入力を検証し、それらの境界を越えるデータは、そうでないと証明されるまで敵対的なものとして扱います。しかし、よくテストされたモジュールの内側では、ロジックを隠し本物の失敗を抑える冗長なチェックを、すべての行に覆いかぶせてはいけません。ルールは単純です。境界で守り、その内側では信頼する。正しいプログラムでは決して偽にならないはずの不変条件を文書化し徹底するには、アサーションを使います。実行時に正当に起こりうる条件には、例外とエラー処理を使います。二つを分けておきます。アサーションはプログラマの前提を守り、エラー処理は予期される失敗を管理します。
エラーを明示的に扱い、安全に失敗する
エラーごとに、何をするかを意図して決めます。回復、リトライ、伝播、またはフェイルファスト。例外を黙って握りつぶしたり、返されたエラーを無視したりしてはいけません。抑え込まれた失敗は、後で謎の欠陥として戻ってきます。失敗が診断できるよう、エラーメッセージとログに文脈を残します。安全や市民に関わる重要なシステムでは、壊れた状態で続けるのではなく、安全で既知の状態に失敗します。エラーの経路にも正常系と同じだけの思考を与えてください。本番では、エラーの経路こそが信頼が勝ち取られたり失われたりする場所だからです。
構築の間に品質を作り込む
品質は後から検査して入れるものではなく、作り込むものです。コードと並行してユニットテストを書き、静的解析とリンターを継続的に実行し、関数を推論できる小ささに保ちます。自明な名前と構造を使い、コメントが「何を」ではなく「なぜ」を説明できるようにします。コードを住みやすく保つため、進めながらリファクタリングします。コードレビュー(2.5章)は人間による最後の砦ですが、品質の大半は、レビューが始まる前にすでにそこにある必要があります。
構築ツールを選び、標準化する
ツールチェーン(コンパイラ、ビルドシステム、フォーマッター、リンター、静的アナライザー、デバッガー、依存関係マネージャー、IDEの設定)を標準化し、すべてのエンジニアが一貫した再現可能な環境で働けるようにします。これらのツールをパイプラインに組み込み、品質チェックが任意でないようにします。AI支援のコーディングツールは意図して取り入れ、その出力を、他のコードと同じ標準、レビュー、テストを通過しなければならない下書きとして扱います。
トレードオフ: 長所と短所
| 実践 | 長所 | 短所 |
|---|---|---|
| 積極的な複雑さの最小化 | 読みやすく、変更しやすく、欠陥率が低い | 遅く感じられうる。誤って適用すると過剰な抽象化のリスク |
| 広範な防御的チェック | 悪い状態を早期に捉える。頑健な境界 | ロジックを散らかす。やりすぎると本物のバグを隠しうる |
| 不変条件のためのアサーション | 前提を文書化し徹底する | 一部の本番ビルドでは無効化される。エラー処理ではない |
| ライブラリの大幅な再利用 | 所有するコードが少ない。デリバリーが速い | 依存関係のリスク、結合、サプライチェーンへの露出 |
| 厳格な標準とリンティング | 一様で摩擦の少ないコードベース | 前もっての設定。個人には硬直的に感じられうる |
| テスト容易性のための構築 | 検証可能で変更しやすいコード | 一部が儀式と見る間接層を加えうる |
構築における中心的なトレードオフは、短期的な速度と長期的な変更容易性です。近道をすること(エラー処理を飛ばす、複雑さを許す、標準を無視する)はその瞬間には速く感じられ、システムの寿命にわたってはほぼ常にもっと高くつきます。反対の失敗は過剰設計です。過度の防御、憶測による抽象化、誰も必要としない一般性。熟練した構築は中間に宿ります。可能な限り単純に、境界が求めるだけ防御的に、それ以上にはならず。
チームで議論すべき問い
「複雑すぎる」の、私たちの共有された具体的な定義は何で、マージ前にどこでそれを徹底していますか。 「複雑さを最小化する」は構築の中心的な規律ですが、スローガンとしては、締め切りとのあらゆる議論に負けます。何百人もが一つのコードベースに書き込む大きなチームでは、複雑さは測定可能でなければならないので、実際に行動するシグナルに合意してください。関数の長さ、ネストの深さ、循環的複雑度、一つの変更を理解するために読み手が頭に保たなければならないことの数。最悪の例を会議に持ち込み、現在のレビューがそれを捉えたかを問います。答えは、パイプラインのゲートかレビューのチェックリストの項目になるはずです。ツールによって徹底される閾値は、意志の力によって徹底される原則より価値があり、次の採用者を、誰も安全に触れないコードのゆっくりした蓄積から守るからです。
本番で、私たちのエラーの経路は設計したとおりに振る舞い、最後に意図してそれを動かしたのはいつですか。 構築の助言は、エラーの経路に正常系と同じだけの思考を与えるよう言いますが、エラーの経路は通常、所有するコードの中で最もテストされていない部分であり、市民に関わる、あるいは安全に関わるシステムでは、信頼が勝ち取られたり失われたりする所です。抑え込まれた例外や無視された戻り値のコードは、数週間後に謎の欠陥になり、「安全な状態に失敗する」は、それが起こるのを一度も見たことがなければ守れない約束です。インシデントの履歴を持ち込んでください。過去の障害のうち、握りつぶされたエラーやテストされていない回復経路に行き着いたものがいくつあったか。行動は、失敗を意図してテストすること(拒否されたカード、タイムアウト、不正な入力を注入する)と、すべてのエラーが処理されるか、文脈とともにログに残されるか、伝播されることを求め、黙って落とされないようにすることです。
コードベースのどの部分がテストしにくく、その難しさは設計について何を教えていますか。 テストに抵抗するコードは、ほぼ常に、状態を隠すか、誤った依存関係に結合するか、やりすぎているコードなので、テスト容易性は、QAの後付けではなく、設計のシグナルです。長寿命の企業システムでは、今日テストするのが苦痛なモジュールが、五年後に見知らぬ人が変更を恐れるモジュールなので、これが重要になります。チームがテストを書くのを恐れているクラスやサービスを持ち込み、なぜかを問ってください。状態が隠れているのか、依存関係を置き換えられないのか、関数が三つの仕事をしているのか。答えは、純粋関数、明示的な依存関係、小さな単一目的の単位に向けたリファクタリングを導くはずです。コードを検証可能にすることは、理解可能で変更が安いものにすることと同じ仕事だからです。
外部ライブラリを再利用するのと、その機能を自分たちで作るのをいつ選び、引き受けるサプライチェーンのリスクは誰が所有しますか。 信頼できるライブラリに手を伸ばすのは、基礎的なロジックを再発明するより速いですが、加えるすべての依存関係は、制御できず、簡単には監査できず、侵害された日にパッチを当てなければならないコードです。大きなチームでは、危険は、百人のエンジニアがそれぞれ自分の推移的な依存関係を持ち込み、コードベースが実際に何を動かしているのか誰も言えなくなることです。依存関係の一覧を持ち込み、三つの具体的なことを問ってください。保守されていないライブラリがいくつあるか、既知の脆弱性を抱えるものがいくつあるか、完全に自前で所有できるほど単純なロジックを包んでいるものがいくつあるか。相反する考慮は本物です。自前の暗号や日付処理を書くことはほぼ常に、実戦で鍛えられたライブラリより悪いので、目標は全面的な回避ではなく、意図した再利用の方針です。企業や政府の設定では、調達とライセンスのコンプライアンスの角度を加えてください。審査されていない依存関係は、義務と両立しないライセンスや、監査人が受け入れない来歴を持ちうるからです。
AIが生成したコードを、人間が書いたコードと同じ構築標準にどう従わせ、重要なときに両者を見分けられますか。 AIコーディングアシスタントはもっともらしい下書きを素早く生み出し、誘惑は、コンパイルでき、イディオム的に見えるからと、その出力を完成品として扱うことです。本章のルールは、生成されたコードは他のものと同じレビュー、テスト、標準を通過するというもので、大きなチームはそのルールを、願望ではなく運用可能にしなければなりません。最近出荷されたAI支援の変更の例を持ち込み、それぞれがテストを伴い、静的解析を通り、提出した人間に本当に理解されていたか、それとも信頼で通されたかを問ってください。相反する圧力は速度です。これらのアシスタントは確かに生産的で、すべての提案を遅くすれば利益を捨てるからです。規制された文脈や政府の文脈では、来歴と説明責任の角度を加えてください。コードの一行について誰が責任を負うか、生成された断片が、答えられないライセンスや著作権の問題を抱えていないかを証明しなければならないかもしれないからです。
構築のツールチェーンは本当に標準化されパイプラインで徹底されていますか。それとも、個々人が互換性のない環境で働き続けていますか。 フォーマッター、リンター、静的アナライザー、ビルドシステム、依存関係マネージャーの共有のツールチェーンは、コードが一つの声として読まれ、チェックがどこでも同一なので、エンジニアが見慣れないサービスの間を自信を持って移動できるようにします。それがずれると、すべてのチームが独自の設定を再発明し、レビューの時間はスタイルの議論に費やされ、あるチームのアナライザーなら捉えたはずの欠陥が、別のチームで通り抜けます。すべてのコミットで標準のチェックを実行していないリポジトリのリストを持ち込み、それぞれがなぜ外れたかを問ってください。緊張は、単一の義務づけられた設定が、本当に異なるニーズを持つチームには硬直的に感じられうることなので、どこで一様性が摩擦に値し、どこで文書化された例外でよいかを決めてください。大きな企業や公的機関では、これを再現性と監査に結びつけてください。管理されたツールチェーンからバイト単位で再現できないビルドは、何年も後に評価者に弁護できないものだからです。
セクター別の視点
スタートアップ。 速度が勝つので、初日に共有のフォーマッターとリンターを導入し、唯一の外部境界で入力を検証し、内部のコードはすべての行で防御的にするのではなく、きれいに保ちます。憶測による抽象化と重いプロセスは省きます。2、3人のエンジニアなら、チーム全体がコードベースを頭に収められ、本当のリスクは、その共有された記憶より長生きする複雑さです。基礎的なものは信頼できるライブラリに頼り、うまく所有できる限りの少ないコードだけを書きます。
小規模事業者。 専任のビルドエンジニアはおらず予算も厳しいので、既存のツールが無料で徹底する規約を好みます。言語に同梱されるフォーマッターとリンター、妥当な既定、誰もが覚えられる少数のルール。保守する人員を配置できないインフラを作るのではなく、よく保守されたライブラリを買うか採用します。限られた規律を、無視すると最も痛い二つのことに使います。境界での入力の検証と、エラーを黙って握りつぶさないこと。
大企業。 何百人ものエンジニアが共有のコードに書き込むなか、優先事項は一様性と徹底です。パイプラインに組み込まれた標準のツールチェーン、静的解析のゲート、どこでも適用される境界検証のルールで、人々がサービス間を自信を持って移動できます。依存関係とサプライチェーンのリスクを、チームごとの即興ではなく、統治されたプロセスとして管理し、アサーションを使って、すべてのチームにわたって成り立たなければならないドメインの不変条件を符号化します。構築の標準を、数十年と人員の入れ替わりを通じてコードベースを住める状態に保つ基盤として扱います。
政府。 長寿命で市民に関わる重要なシステムは、規律ある構築を保証と説明責任の問題にします。法律のような不安定なルールを安定したインターフェースの背後に隔離し、変更が局所に留まり要求に追跡できるようにし、壊れた状態で続けるのではなく、安全で既知の状態に失敗し、すべてのモジュールに、監査の証拠にもなるテストを付けて出荷します。調達と透明性の義務は、ツールチェーン、依存関係、エラー処理が、何年も後に着任する公務員や外部の監査人がコードの正しさを検証できるほど、よく文書化されていなければならないことを意味します。
事例
スタートアップ。 3人のエンジニアのスタートアップは、初日に共有のフォーマッターとリンターを設定してすべてのコミットで実行するので、契約者が加わってもコードベースは一つの声として読めます。APIの境界で入力を検証し、外部からのものすべてを敵対的として扱いますが、内部のロジックは、冗長なチェックで覆うのではなくきれいに保ちます。決済のウェブフックが失敗し始めたとき、例外が黙って握りつぶされたことは一度もなく、エラーメッセージが原因を直接指すのに十分な文脈を持っているので、修正は速く済みます。設定全体に午後一つかかっただけで、次の採用者の最初の週を惨めにしたはずの、複雑さのゆっくりした蓄積を省きました。
大企業。 グローバルな決済会社が、何百人ものエンジニアにわたって共有のツールチェーンを徹底しています。すべてのコミットでの自動のフォーマットとリンティング、パイプラインの静的解析のゲート、すべての外部入力がサービスの境界で検証されるというルール。ドメインのロジックは、「元帳のエントリは常に釣り合う」のような不変条件を徹底するためにアサーションを使い、拒否されたカードのような実行時の条件は、明示的でログに残る結果として扱われます。標準が一様で、エラーが決して黙って握りつぶされないので、エンジニアは見慣れないサービスの間を自信を持って移動でき、本番のインシデントはログから直接診断できます。
政府。 国の税務当局が、変わる法律のもとで数十年稼働すると期待される、長寿命の査定システムを構築します。構築は各税のルールを安定したインターフェースの背後に隔離するので、年次の法改正は局所に留まり、要求(2.8章)に追跡できます。防御的な検証が、市民向けのすべての入力を守ります。エラーの経路は、誤った査定を黙って発行することのない安全な状態に失敗します。すべてのモジュールが、監査の証拠としてのユニットテストとともに出荷されます。構築が標準化され、よく文書化されているので、新しい公務員は、とうに去った前任者が書いたコードを安全に保守できます。
ビジネスケース: 動機、ROI、TCO
規律ある構築の見返りは、ソフトウェアを安く安全に変更し続けられる能力であり、そこでシステムの総所有コストの大半が決まります。ソフトウェア経済学の研究は一貫して、システムの生涯コストの大半は保守であり、保守コストはコードがどれだけ理解しやすく変更しやすいかに支配されることを示しています。複雑さの最小化、エラーの明示的な処理、標準の遵守は、将来のあらゆる変更とあらゆる本番インシデントのコストを直接下げます。
採用のコストはささやかで、大半が前もってのものです。標準を設定し、リンターとアナライザーを組み込み、検証可能で防御的なコードを書く習慣を築くこと。一方、放置のコストは複利で増えます。複雑さは、変更が遅く触れるのが危険なコードに蓄積します。黙ったエラーは、高価な本番インシデントになります。一貫しないスタイルは、あらゆるレビューとあらゆるオンボーディングの労力を増やします。リーダーシップに論拠を示すには、構築の質を、変更失敗率、平均復旧時間、欠陥の流出率、オンボーディング時間に結びつけてください。構築の規律はそのすべてを直接改善します。
アンチパターンと落とし穴
- 複雑さの忍び寄り: 誰も理解できなくなるまで、巧妙で、深くネストした、広がったコードを積み上げること。
- エラーの黙った握りつぶし: 失敗を将来の謎に変える、空のcatchブロックと無視された戻りコード。
- 防御的プログラミングのやりすぎ: ロジックを埋もれさせ、本物の欠陥を隠す、あらゆる所の冗長なチェック。
- アサーションとエラー処理の混同: 実行時の条件にアサーションを使う、あるいはプログラマの不変条件に例外を使うこと。
- コピー&ペーストの構築: 再利用する代わりにロジックを複製し、修正を多くの場所で行わなければならなくなること。
- 憶測による一般性: 決して来ないニーズのための抽象化と設定可能性を作ること。
- 標準の無視: 各エンジニアが自分のやり方でコーディングし、コードベース全体で認知負荷を増やすこと。
- テストされない構築: 一緒にテストを書かずにコードを書き、検証を決して来ないフェーズに先送りすること。
成熟度モデル
- レベル1(開始): 構築は場当たり的で反応的です。複雑さとエラー処理は個人によって異なり、標準はほとんどなく、黙った失敗が一般的です。
- レベル2(発展): コーディング標準、フォーマッター、リンターが存在し、基本的なエラー処理とユニットテストが期待されていますが、実践は一貫せず、各チームが異なって適用します。
- レベル3(標準化): 複雑さの最小化、境界の検証、明示的なエラー処理、テスト容易性が、パイプラインとレビューで、組織全体で文書化され、徹底されており、コードベース全体が、一人の注意深い作者が書いたかのように読めます。
- レベル4(管理): 構築の質がベースラインに対して測定されます。チームは循環的複雑度、欠陥の流出率、変更失敗率、エラー経路のテストカバレッジ、コードレビューの指摘を追跡し、意見ではなく傾向に基づいて行動します。
- レベル5(オーケストレーション): 構築は継続的に改善され、組織全体に統合されています。防御的なパターン、標準、指標がリファクタリングとツールにフィードバックされ、AI支援のツールは同じ品質ゲートのもとで動き、言語、規制、リスクの変化に応じて実践が適応します。
議論のためのアイデア
- コードベースで偶有的な複雑さが最も蓄積するのはどこで、どんな構築の習慣がそれを生んでいますか。
- 入力をどこで検証し、どこで信頼するかについての、チームの実際のルールは何ですか。
- エンジニアはアサーションとエラー処理を区別していますか。そしてその区別は一貫していますか。
- 品質のどれだけが構築の間に作り込まれ、どれだけが後でレビューやテストで捉えられていますか。
- サプライチェーンのリスクを考えて、ライブラリを再利用するか作るかをどう決めますか。
- AIが生成したコードを、人間が書いたコードと同じ構築標準にどう従わせるべきですか。
要点
- 構築は設計が保守可能なコードになる場所であり、複雑さの最小化がその中心的な規律です。
- 変更のために、検証のために構築します。テスト可能で変更しやすいコードは、理解しやすいコードです。
- 信頼の境界で守り、その内側では信頼し、エラーを黙って握りつぶしてはいけません。
- 不変条件にはアサーションを、予期される実行時の条件にはエラー処理を使い、混同しません。
- ツールとスタイルを標準化し、意図して再利用し、後から検査するのではなく品質を作り込みます。
参考文献とさらなる読み物
- IEEE Computer Society, SWEBOK Guide (Guide to the Software Engineering Body of Knowledge), Software Construction knowledge area
- Steve McConnell, Code Complete: A Practical Handbook of Software Construction
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
- Andrew Hunt and David Thomas, The Pragmatic Programmer
- Martin Fowler, Refactoring: Improving the Design of Existing Code
- John Ousterhout, A Philosophy of Software Design