Steve McConnell『Code Complete 第2版』、読んだ
Steve McConnellの『Code Complete』(第2版、2004年)は、ソフトウェア開発の名著とされる。たとえば、The 25 most recommended programming books of all-time.では第3位に位置している。わたしも昔読んだが、そのときは「よくまとまっているんだろうけれど、特に面白みがない本」という印象だった。ただし、そのときのわたしは遊びのプログラミングはしていたが、堅苦しいオフィスでのソフトウェア開発はしていなかった。
プラクティス本と呼べばいいんだろうか、プログラミング言語を限定せずにプログラミングの汎用的なグッドプラクティスを書いた類の本がある。『プログラミング作法』、『達人プログラマー』、『Clean Code』、『リーダブルコード』、『Code Complete』。近年ではプラクティス本はあまり流行らなくなった印象だが、言語ごとの強力な lint ツールが自動で助けてくれるからだろう。「『Code Complete』を読むこと。それに従って書くこと。レビュー時にそれに従っていないコードを指摘すること」そんな手間より「この lint ツールに従うこと。CI が勝手にエラーにするから」とする方が楽だ。
プラクティス本として見ると、『Code Complete』は一般的なグッドプラクティスを言っているだけなので、初読時と同じ感想で「特に面白みがない」。ただ、『Code Complete』のポイントは、類書と比べると少しアカデミックな堅苦しい書き方と、コーディングだけでなくソフトウェア開発全体にフォーカスしていることにある。
2022年に読み直してみたところ、まずすごい労作だと思う。特に第1版が出版された昔や、第2版が出版された当時は、名著とされるに足る。
ただ、現在となっては、本書の中心的な内容であるコーディングプラクティス部分はすすめづらい。言語中立の書き方で、しかも Visual Basic がサンプル言語の1つとして選ばれているため、利用している言語向けのプラクティス記事を読む方がよい。
『Code Complete』で書かれていることは、適切で大切なことだ。しかし、この本を読んで学べる人はすでにその内容を実践できている実力の人だし、逆にそうでない人はこの本を読んでも説明の具体性が足りなくて実践できない。1/5くらいの分量にして「詳細は参考文献を参照」を連発するか、C++ か C# にでも対象言語を絞って具体的な1つのプロジェクト開発の例を一貫して出して記述するか。どちらかにした方がよそうだ。
面白かったのは、参考文献。ここに Steve McConnell の個性が感じられた。
- Tom Gilb “Principles of Software Engineering Management” (1988)
- Alain Abran et al. “SWEBOK: Guide to the Software Engineering Body of Knowledge” (2001)
- Karl Wiegers “Software Requirements” (2003)
- Len Bass, Paul Clements, Rick Kazman “Software Architecture in Practice” (2003)
- Steve Maguire “Writing Solid Code” (1993)
- Jon Bentley “Programming Pearls” (2000)
- Andrew Hunt, David Thomas “Prgamatic Programmer” (2000)
- KentBeck “Extreme Programming Explained: Embrance Change” (2000)
- David L. Parnas “On the Criteria to Be Used in Decomposing Systems into Modules” (1972)
- Erich Gamma et al. “Design Patterns: Elements of Reusable Objet-Oriented Software” (1995)
- Arthur J. Riel “Object-Oriented Design Heuristics” (1996)
- Bertrand Meyer “Object-Oriented Software Construction” (1997)
- Martin Fowler “Refactoring” (1999)
- Brooks “The Mythical Man-Month” (1995)
- Gerald M. Weinberg “The Psychology of Computer Programming” (1998)
- DeMarco, Lister “Peopleware” (1999)
上はあげられている参考文献の一部にすぎないが、Tom Gilb “Principles of Software Engineering Management” と Arthur J. Riel “Object-Oriented Design Heuristics” への高評価は印象的だった。
McConnell の Construx 社では The Professional Development Ladder を出していて、そこではレベルに応じた書籍を紹介している。たとえば、ソフトウェアアーキテクトのキャリアパス。
読書メモ
第1章 ソフトウェアコンストラクションへようこそ
この章では、この本のテーマであるソフトウェアコンストラクションについて説明している。
ソフトウェア開発は次のプロセスからなるとして:
- 課題定義
- 要求開発
- コンストラクション計画
- ソフトウェアアーキテクチャ(または概略設計)
- 詳細設計
- コーディングとデバッグ
- 単体テスト
- 統合テスト(または結合テスト)
- 統合
- システムテスト
- 保守
そのうち、ソフトウェアコンストラクションは上のほとんどのプロセスを含んではいるものの、重きを置いているのは「作る」プロセスつまり「コーディングとデバッグ」だという。要するに、ソフトウェアコンストラクションという用語は、ソフトウェア開発の言い換えにすぎないんだろうけれど、よりコーディングにフォーカスしているニュアンスが入っている。
ただし、重きを置いていないだけで、コーディング以外もソフトウェアコンストラクションに含まれるから、『Code Complete』ではすべて扱ってはいる。このすべてを扱っているということが偉い。
根本的な疑問として、ソフトウェア開発のプロセスを上のように定義することがどこから来たか、正しいかは分からない。この章については参考資料がない。
第2章 ソフトウェア開発への理解を含めるメタファ
この章ではメタファの説明をしている。
それ自体はプログラミングと関係ない「メタファ」というテーマだけで1章費やしているのは、本のバランスを崩していると思った。読者を「なんだこの本…?」と困惑させるだけじゃないか? そもそも人間の思考活動にとってメタファは大事なので、ソフトウェア開発に限った話じゃない。
第3章 2回測って、1度で切る:上流工程の必要性
この章では上流工程の説明をしている。
ソフトウェア開発の上流工程というのは、課題定義からソフトウェアアーキテクチャまでを指している。上流工程という準備が不足していると、手戻りのコストは最も高くなる。
課題定義
「課題定義」は、プロジェクトビジョン、プロダクト定義などとも呼ばれる。ユーザの言葉で書かれた、システムが解決すべき課題。
要求
「要求」は、要求開発、要求分析、ソフトウェア要求、仕様などとも呼ばれる。
要求仕様をどう書くかは具体例も何もないが、要求のチェックリストは用意されている。自分なりに簡潔化すると:
- 機能要求
- システムへの入力を、入力元・精度・範囲・頻度を含めて明記する。
- システムからの出力を、出力先・精度・範囲・頻度・出力形式を含めて明記する。
- 外部のハードウェアインタフェース、ソフトウェアインタフェース、通信インタフェースを明記する。
- ユーザが実行したいと考えている各タスク、それ使われるデータを明記する。
- 非機能(品質)要求
- ユーザが期待する応答時間を、操作ごとに期待する。
- 処理時間、データ転送の速度、システムのスループットなど、時間に関する検討事項を明記する。
- セキュリティレベルを明記する。
- ソフトウェアの障害の影響、障害から保護すべきデータ、エラーの検出と回復の手順を含め、信頼性を明記する。
- 最低限必要なメモリ容量と空きディスク容量を明記する。
- 特定機能の変更など、システムの保守性を明記する。
アーキテクチャ
「アーキテクチャ」は、システムアーキテクチャ、概略設計とも呼ばれる。
アーキテクチャのチェックリストは用意されている:
- アーキテクチャの概要や理由付けを明記する。
- プログラムのそれぞれの構成単位が受け持つ領域や、他の構成単位とのインタフェースを定義する。
- 主要なクラスの説明を明記する。
- データ設計の説明、データベースの構造を明記する。
- 業務ルールを明記する。
- ユーザインタフェースの設計方針を明記する。
- 入出力の処理方針を明記する。
- リソースに対する使用量の見積もりやリソース管理の方針を明記する。
- セキュリティ用件を明記する。
- パフォーマンス目標の推定値と、機能のスペースと速度を見積もる。
- スケーラビリティの実現方法を明記する。
- 相互運用性に対処する。
- 国際化と地域化の方針を明記する。
- エラー処理方針を明記する。
- フォールトトレランスの手法を定義する。
- システムは技術的に実現可能か考慮する。
- オーバーエンジニアリングへの取り組み方を明記する。
- 購入か構築かの決断を含める。
- 再利用するコードを説明する。
- 予想される変更に適応できる設計にする。
- アーキテクチャの全体的な品質を確認する。
参考資料
最後に参考資料が紹介され、詳細はそちらに任せられている。この本だけでは上流工程の説明が圧倒的に足りないが、それはもう別の本で学べということなんだろうね。
要求・仕様については Karl E. Wiegers の『ソフトウェア要求』を読むべきなんだろうけど、紙版は絶版で、電子版も高い。『ユースケース実践ガイド』も読むべきなんだろうけれど、電子版はなく、紙版は絶版。高かったけど、中古で買った。あとは『SWEBOK』も紹介されている。
アーキテクチャの本はいろいろあるが、今だと『ソフトウェアアーキテクチャの基礎』なんだろうか。買ってないけれど。
ソフトウェア開発手法全般の本は、今はアジャイル系以外絶版しかない。
第4章 コンストラクションの重要な決断
この章では、ソフトウェア開発をする上で選定すべきいくつかのものを説明している。プログラミング言語、プログラミング規約、ツールやフレームワークの選択について。
第5章 コンストラクションにおける設計
設計に望ましい特性:
- 最小限の複雑さ
- 保守性:「保守プログラマを顧客と見なし、見ればすぐわかるようなシステムを設計しよう」
- 疎結合
- 拡張性
- 再利用性
- 高いファンイン:そのクラスを使用するクラスがたくさんあること。
- 高いファンアウト:1つのクラスが使用するほかのクラス数を少なくすること。
- 移植性
- 無駄のなさ
- 階層化:分解のレベルを階層化して、他のレベルを見なくても、1つのレベルを見ればわかるように設計する。
- 標準化手法
設計のレベル:
- ソフトウェアシステム
- サブシステムまたはパッケージへの分割
- 業務ルール
- ユーザインタフェース
- データベースアクセス
- システムへの依存部分
- クラスへの分割
- ルーチンへの分割
- ルーチンの内部設計
構成要素の設計:
- 現実世界のオブジェクトを見つけて、その属性・インタフェースを定義する
- 一貫した抽象化を行う
- 実装詳細をカプセル化する
- (設計が単純になる場合)継承する
- 情報を隠蔽する
- 変更可能性が高い箇所を特定する(業務ルール、ハードウェアへの依存部分、入出力など)
- 疎結合を維持する
- デザインパターンを探す
参考資料では、情報隠蔽については David L. Parnas の3つの論文を「現時点で最も優れた資料である」として紹介している。“On the Criteria to Be Used in Decomposing Systems into Modules” (1972)、“Designing Software for Ease of Extension” (1979)、“The Modular Structure of Complex Systems”(1985)。
第6章 クラスの作成
ADT、よいクラスの抽象化、よいクラスのカプセル化、継承の問題、クラスの利点について。『Effective C++』や『Effective Java』のようないわゆる Effective 本やプラクティス本で書かれているようなことが、言語中立的に詰め込まれている。
参考資料には Bertrand Mayer の “Object-Oriented Software Construction” が紹介されている。訳書が高い。
第7章 高品質なルーチン
ルーチンの利点、ルーチンの凝集度、ルーチン名、ルーチンの長さ、ルーチンの引数、マクロとインラインルーチンについて。これも Effective 本やプラクティス本のような内容が詰め込まれている。
第8章 防御的プログラミング
- 無効な入力
- 外部ソースからのデータの値をすべて確認する
- ルーチンのすべての入力引数の値を確認する
- 不正な入力を処理する方法を決定する
- アサーション
- 発生してはならい状況にアサーションを使う
- 事前条件と事後条件の文書と検証にアサーションを使う
- エラー処理
- 当たり障りのない値を返す
- 次に有効なデータで代用する
- 全開と同じ答えを返す
- 有効な値のうち、最も近いもので代用する
- ファイルに警告メッセージを記録する
- エラーコードを返す
- エラー処理ルーチン・オブジェクトを呼び出す
- エラーが発生した場所でエラーメッセージを表示する
- ローカルで最もうまくいく方法でエラーを処理する
- 処理を中止する
- 例外
- 無視すべきでないエラーに例外を使う
- 例外的な状況でのみ例外を使う
- ライブラリコードが投げる例外を知る
- バリケードによるエラーの被害の囲い込み
- 入力データは入力されたときに正しい型に変換する
第9章 擬似コードによるプログラミング
ルーチンの詳細設計として擬似コードを書く、擬似コードプログラミングプロセス (PPP) について。PPP 以外の方法として、テスト駆動開発、リファクタリング、契約による設計を出している。
第10章 変数の使用
- 変数は宣言時に初期化する。
- 変数のスコープは最小限に抑える。
- 変数の値は適切なタイミングでバインドする。
- 変数の目的は1つだけにする。
第11章 変数名の力
変数や関数の名前の付け方について。
第12章 基本的なデータ型
- 数値
- 0 による除算はエラーになる
- 異なる型の値の演算は、暗黙の型変換が行われる
- 整数
- 整数の除算は、端数が切り捨てられる
- 整数には最大値があるため、演算で桁あふれすることがある
- 浮動小数点数
- 浮動小数点数には有効桁数があるため、大きさが極端に異なる数同士の演算は情報落ちすることがある
- 浮動小数点数の演算では誤差が出るため、等価にならないことがある
第13章 特殊なデータ型
構造体、ポインタ、グローバル変数について。
第14章 ストレートなコードの構成
- ストレートなコードは、文の実行順序の依存性を明白にする。
第15章 条件文の使用
if/else, case 文について。
第16章 ループの制御
while, for 文について。
第17章 特殊な制御構造
return, goto 文と再帰について。
第18章 テーブル駆動方式
テーブル駆動方式について。テーブル駆動方式について書かれた本はほかにもありそうだが、なんだろう。『プログラミング作法』にあったような気がしたんだけれど(Rob Pike はその後 Go で Table-Driven Testing を推進している)、手元に本がないので分からない。
第19章 制御構造の問題
- 論理式は、肯定的な単純な式にして、かっこを使って明確化する
- 数値を含んでいる式は、数直線の順番に並べる
- 深いネストは減らす
- 構造化プログラミングは、制御構造を連続・選択・反復の3つとする
第20章 ソフトウェアの品質
品質保証について。
第21章 コラボレーティブコンストラクション
ペアプログラミング、ソフトウェアインスペクション(ピアレビュー)について。
第22章 デベロッパーテスト
- ルーチンの直線パスを 1 として、if/while 文 や case ごとや and/or 式があるたびに 1 を加える。その結果が最低限必要なテストケースの数となる。
defined -> used
データフローのすべてのパスをテストする。- 過去に発生したよくあるエラーのテストケースを足す。
- 境界条件をテストする。
- 代表的な値をテストする。
第23章 デバッグ
デバッグについて。
第24章 リファクタリング
Martin Fowler の “Refactoring” をもとに、“Code Complete” では以下のリファクタリングを概略している:
- データレベルのリファクタリング
- マジックナンバーを名前付きの定数に置き換える
- 変数をより明白なものに変える
- 式をインラインにする
- 式をルーチンに置き換える
- 中間変数を導入する
- 多目的変数を複数の単一目的変数に変換する
- ローカルの目的には引数ではなくローカル変数を使用する
- データプリミティブをクラスに変換する
- 一連の型コードをクラスまたは列挙に変換する
- 一連の型コードをスーパークラスとサブクラスに変換する
- 配列をオブジェクトに変換する
- コレクションをカプセル化する
- 従来のレコードをデータクラスに置き換える
- ステートメントレベルのリファクタリング
- 論理式を分解する
- 複雑な論理式を分かりやすい名前の付いた論理関数にする
- 条件文に分散している重複するコードを1つにまとめる
- ループ制御変数ではなく
break
やreturn
を使用する - ネストした
if-then-else
ブロクで答えが分かったら、戻り値を代入せずに、すぐにreturn
する - 条件文、特に
case
文の繰り返しをポリモーフィズムで書き換える nukll
値を評価するのではなくnull
オブジェクトを生成して使用する
- ルーチンレベルのリファクタリング
- ルーチンやメソッドを抽出する
- ルーチンのコードをインラインにする
- 長いルーチンをクラスに変換する
- 複雑なアルゴリズムを単純な雨後リズムで代用する
- 引数を追加する
- 引数を削除する
- 問い合わせと更新を分離する
- 同じようなルーチンは引数を介在させてまとめる
- 入力引数によってふるまいに異なるルーチンを分離する
- 特定のフィールドではなくオブジェクト全体を渡す
- オブジェクト全体ではなく特定のフィールドを渡す
- ダウンキャストをカプセル化する
- クラス実装のリファクタリング
- 値オブジェクトを参照オブジェクトに変更する
- 参照オブジェクトを値オブジェクトに変更する
virtual
メソッドをデータの初期化に置き換える- メソッドまたはメンバデータの配置を変更する
- 特殊なコードをサブクラスとして抽出する
- 同じようなコードをスーパークラスにまとめる
- クラスインタフェースのリファクタリング
- メソッドを別のクラスに移動する
- 1つのクラスを2つに分ける
- クラスを削除する
- 委譲を隠蔽する
- 中間オブジェクトを削除する
- 継承を委譲に置き換える
- 委譲を継承に置き換える
- 外部ルーチンを導入する
- 拡張クラスを導入する
- 公開されているメンバ変数をカプセル化する
- 変更できないフィールドのセッタ・メソッドを削除する
- クラスの外側で使用されないメソッドを隠蔽する
- 使用されないメソッドをカプセル化する
- スーパークラスとサブクラスの自走が非常によく似ている場合は1つにまとめる
- システムレベルのリファクタリング
- 制御できないデータについては、最も信頼のおけるデータソースを作成する
- 一方向のクラス結合を双方向のクラス結合に変更する
- 双方向のクラス結合を一方向のクラス結合に変更する
- 単純な雨後コンストラクタではなくファクトリメソッドを提供する
- エラーコードを例外に置き換える、または例外をエラーコードに置き換える
第25章 コードチューニング戦略
コードチューニング戦略について。
第26章 コードチューニングテクニック
コードチューニングテクニックについて。
第27章 プログラムサイズが及ぼす影響
プロジェクトが大きいほど、コミュニケーションコストが高くなり、生産性は下がる。プログラムサイズがフェうrほどエラー数は増える。
第28章 コンストラクションの管理
コーディングプラクティス、変更管理、工数の見積もり、ピープルウェアについて。
第29章 統合
インクリメンタルなインテグレーションと、デイリービルドとスモークテストの自動化について。
第30章 プログラミングツール
CASE ツール、IDE、一括検索・置換、diff、フォーマッタ、スニペット、linter、リファクタリングツール、バージョン管理ツール、ビルドツール、デバッガ、テストルールなど。
第31章 レイアウトとスタイル
コードのインデントのレイアウトについて。
第32章 読めばわかるコード
コードコメントについて。
第33章 個人の資質
必要な個人の資質は、謙虚さ、好奇心、知的な誠実さ、創造性と規律、啓発的な怠惰。
第34章 ソフトウェア職人気質とは
第35章 さらに情報を得るには
様々な参考文献。