Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】Code Complete 第2版 上 完全なプログラミングを目指して

ソフトウェア開発の不朽のバイブルのひとつ

 コード・コンプリート。「エンジニア・プログラマー向けお勧め書籍X選」系の記事や業界の一線で活躍している方のインタビューや影響を受けた本・おすすめ本などで必ずと言っていいほど出てくる頻出の名著ですが、1ヶ月ぐらいかかってようやく読み終わったので感想など書いてみます。

 著者のスティーブ・マコネル氏はアメリカのシアトル大で修士号を取得したりMicrosoftボーイングでも働いていたソフトウェア工学の第一人者。初版が出たのが1993年、オブジェクト指向の考え方が入った第2版がその10年後、日本語版の2版が出たのが2005年。上巻が460P、下巻が360P余りと合計1000ページを超える(!)、手持ち武器に使えそうな分厚い本です。

本の概要

 タイトルは「コード」となっていますがプログラミングだけではなく、上流は基本や要件定義から始まりプログラミングを徹底的に、最後はテスト、果てはプログラマーの職人気質まで、開発についてまるっと全部まさにコンプリート、ソフトウェア開発方法論を幅広く網羅した入門書となっています。
 全7部で全35章、章の下に節、項まであって学術書のような趣もあります。それぞれの項は長くないので丸ごと一気に読もうと思わなくても分けて読んでいけます。

 対象は作中では経験豊富なプログラマ、技術指導者、そして独学で学んだ人や学生もとあります。本自体が特定の言語や特定の技術に左右されない内容なので、最も価値ある援助は作業や言語に関係なく使用できるプラクティスだ...と冒頭でもしっかり述べて、時代を超えて役に立つような不変の匠の技について、がっつりと解説してあります。

 全7部の「第2部 高品質なコードの作成」から本の中にたびたび出てくるサンプルコードの言語はCやC++C#JavaVB。イケてないコード(ネットで言うところの所謂クソコードw)例はプログラマーが驚愕している絵の「CODING HORROR」アイコンと一緒に何度も出てくるのですが、どうしてイケてないホラーでテリブルなコードなのかは言語を問わず経験者ならだいたいパッと見れば分かるようなものが多く、それぞれの言語の文法や細かな仕様まで知らなくても大丈夫です。何を隠そうワタクシも昔ポインタで挫折してCやC++はマスターしてないのですが、本書の内容は理解できました。

 文中や章の末尾のあちこちに参考になる文献もずらりと並んでいます。だいたい20世紀の比較的古め、英語の資料が多いですが日本語訳されたもの、近年でもよく名前が挙がる名著も時々登場します。
 また文中でも「この調査の統計ではプロジェクトの何%が実は~」という感じで、裏付けのある実際の数字がたくさん出てきます。独習でのスキル向上以外にも、勉強会や社内で何か資料を作ったりするときの参考文献としても役立つかもしれません。

 『達人プログラマー』などと同じく、偉人の名言もたびたび登場して何やら本物感を醸し出しています。歴史上の人物からソフトウェア業界の有名人、果ては無名の歴戦のプログラマーまで。

新装版 達人プログラマー 職人から名匠への道

新装版 達人プログラマー 職人から名匠への道

『リーダブルコード』や『Clean Coder』ほど砕けた感じではなく基本的には真面目寄りの本なのですが、所々にユーモアが入っていてニヤリとします。
 随所に「あるプロジェクトで~」と実際にあった笑い話や苦労話も添えられており、経験者ならあるある話で共感できるでしょう。あーきっとこの話はMicrosoft社内での開発から分かったノウハウなんだろうなあと思ったり。

内容概説

 しょっぱなから著名人による「『Code Complete』への賛辞」が多数載っており、読者に「こんなすごい聖典をこれから読むんだ...(震)」的な感動のガクブル感を与えますが、簡単に感想を記してみます。

第1部 基礎を固める

第1章 ソフトウェアコンストラクションへようこそ

 実装周りのフェーズを本書では「コンストラクション」と位置付けて丹念に述べていきます。日本の開発業界の標準に照らすと詳細設計、コーディング、デバッグ単体テスト結合テストのあたりでしょうか。マネジメントの人がやる計画付け、アーキテクチャ、総合テストも多少絡んできます。
この「コンストラクション」こそが最重要、開発の大部分を占める中心的な活動であり、専念すると生産性が向上、ソースコードが最大のドキュメントとなる、省略されずに必ず実行される最重要の作業だ...と定義しています。
 日本だと工程の名称としてはよく「コーディング」を使ったり、よくdisられるSI系の世界だと「コーダー」には技術や優秀さが絡まず単に仕様をコードに変換するだけな蔑視的な意味が含まれたりすることもありますが、本書ではコーディングという言葉は使っていませんね。

第2章 ソフトウェア開発への理解を深めるメタファ

 ソフトウェア開発でもメタファ=たとえは重要だという話。文を書く、農場でシステムを育てる、真珠の養殖、ソフトウェアを「構築(build)」する、で家を建てる仕事になぞらえたメタファが詳しく述べられています

第3章 2回測って、1度で切る:上流工程の必要性

 プログラマーが行うコンストラクション最重要と訴えつつも、その準備の設計も大事としっかり論じています。重要さを上のエライ人に訴えるポイントまで書いてあって、20世紀の開発でも設計なしでコーディングに行っちゃってどうしようもなく炎上するプロジェクトはあったんだろうなと分かります。
 設計の話でよく出てくる、欠陥の侵入の検知が遅くなればなるほど修正のコストが10~100倍になる話も。反復、逐次型の開発形態(アジャイルウォーターフォールの話)の話も出てきます。
 要求は変わるものだ、アーキテクチャも重要だという話は的を射る矢の絵が添えられてますが犬が可愛いですね。
 要求の定義やソフトウェアアーキテクチャ、ソフトウェア開発手法全般の参考書籍が章末で紹介されていますが、日本語で読める有名な本も多数載っています。

第4章 コンストラクションの重要な決断

 プログラム言語の選択で生産性、品質は大きく変わるという話。ここで本でしか見たことのないAdaやアセンブラ、FORTLANにCOBOLSmalltalkも上がってくるのに時代を感じます。
 PerlはもちろんJavaScriptPHPPythonも登場します。ちなみに日本では盛り上がったけれど世界的にはそうでもないのか、Rubyは出てきませんね。
 また、単に言語仕様に従って書く、言語がサポートする構造の中でしか限定して考えないことを「言語の中でのプログラミング」。何を表現したいかを決めてそれを言語でどう実現するかを自分で考えるのを「言語の中へのプログラミング」と表現し、この考え方は全体を通じて何回も出てきます。
 自分なりの設計規約やルール、クラスライブラリや共通化を工夫したりして、言語の枠内に思考を囚われずに頭脳を駆使して立ち向かっていくのが上級エンジニアだ……ということですね。

第2部 高品質なコードの作成

第5章 コンストラクションにおける設計

 いよいよがっつり深遠なプログラミングの世界へ突入していきます。設計は厄介で、

  • 開発規模によってはキーボードの前で行われることもある
  • ルーズで間違ったり戻ったりする
  • 時間などの制限がある
  • 決定論的(=正解が1つでない)
  • ヒューリスティックである(発見的、予測可能な結果保証がなく大雑把、試さないとわからない、ひらめき的である)
  • 創発的である(話し合いやレビュー、コードの改良で進化していく)

と、設計せずにプログラミングに突入しがちな初心者に設計の重要さを伝えています。

「すべての設計の目標は複雑な問題を単純な問題に分割していくことである」、という本書全体を通じた定義がここで出てきます。
 設計に望ましい特性は……

  • 最小限の複雑さ
  • 保守性
  • 疎結合
  • 拡張性
  • 再利用性
  • 高いファンイン(そのクラスをたくさんのクラスが使うこと)、
  • 低いファンアウト(1つのクラスが使う他のクラスの数を少なくする)
  • 移植性
  • 無駄のなさ
  • 階層化

設計のレベルは……

  • システム
  • サブシステム
  • パッケージへの分割
  • クラスへの分割
  • クラス内でルーチンに分割
  • ルーチンの内部設計

 そして設計は非決定論的なプロセス(=ヒューリスティクス)であると論じ、現実世界のクラス化の話へ。他の本にもよく出てくる家の絵とか氷山の絵が面白いですね。抽象化やカプセル化疎結合、その後にデザインパターンの話も大まかに出てきます。  設計のプラクティスとしては反復、分割攻略、トップダウンボトムアップ方式の対比、プロトタイプ、複数人でやるコラボレーティブ設計を紹介。
 そしてどれだけ設計すれば十分なのか? という話で、チームのスキルや規模で設計の詳細レベルとドキュメントの公式性を表にまとめているのが興味深いです。
 最後の参考資料では「ソフトウェアの設計の分野は、資料の宝庫である」と題し、参考文献が多数並んでいます。ここも日本語化された本が多いですね。

第6章 クラスの作成

 現在選ばれているフォントや原子炉の冷却装置を例にとり、抽象的なものは抽象データ型(ADT: Abstract Data Type)としてクラス化してメソッドでアクセスすると値を変更するより分かりやすいよという話。適用対象は下位レベルのデータ型(リストやスタック、キューとか)やファイルなどが例に挙げられています。これは2010年代現在の高級言語なら大体実現されているものですね。

 「インターフェース」は言語仕様のInterfaceでなく、クラスにどういう単位でメソッドをまとめていくかの話で詳しく述べられています。このへんから悪いコード例のCODING HORRORがよく出てくるようになり、不吉な匂いがプンプンする悪い見本のコードが多数挙げられています。
 継承の話は有名な「リスコフの置換原則」や多重継承の話。継承の話は悩む人が多いのかしっかり記述されています。継承は、プログラマの第一の責務である複雑さへの対処にマイナスに働く傾向がある、と書いてるのが面白い。だいたい最近のオブジェクト指向の設計の本などでは継承より包含とか移譲だと言われていますが、なんでもかんでも継承しちゃアカンのだというのが理解できます。

 なぜクラス化するのか、なぜオブジェクト指向で作るのかという話もはっきり定義しています。複雑さを分離、実装の詳細を隠蔽、変更による影響を限定、グローバルデータを隠蔽、制御を一元化、コードの再利用を促進、特定のリファクタリングを実行……と、だいたい一般的に言われている理由ですね。
 時々Qiitaやネットの記事やコードのネタ話でよく出てきますが、望ましくないクラスで全知全能の巨大な「ゴッド」クラスを作っちゃダメよと本書でも述べています。参考文献はこの章はC++JavaVBごとに挙げられています。

第7章 高品質なルーチン

 クラスの話をしたので今度はその中へ。メソッド、関数、プロシージャのことを本書では総称して「ルーチン」と称しています。
 章の冒頭からCODING HORRORで激ヤバなルーチンを上げつつ順次解説が進みます。
 ルーチンを作る理由は:

  • 複雑さの低減
  • 中間部分を分かりやすく抽象化
  • コードの重複を下げる
  • サブクラスを作成しやすくする
  • 処理順序を隠蔽
  • ポインタの操作を隠蔽
  • 移植性の向上
  • 複雑な論理評価を単純に
  • パフォーマンスを向上

 ルーチンレベルの設計では凝集度(cohesion: 強度)として機能的凝集度、情報的凝集度、連絡的凝集度、時間的凝集度、手順的凝集度、論理的凝集度、暗号的凝集度、を解説。
 ルーチンの名前付けの話題は『リーダブルコード』でもお馴染み、命名テクニックや反意語など。
 ルーチンの長さについては昔から議論や論文も多数あるそうですが、この本では最終的には、「分かりやすさの上限に達する200行(コメント、空白除く)以上のルーチンを書く場合は注意が必要」だと結論づけています。  その他、

  • 引数は入力、変更、出力の順に配置すると分かりやすい(これは最近の他の本ではあまり見ないような?)
  • 独自defineが可能な言語ならIN/OUTを定義してみる
  • 複数ルーチンで似たような引数を使うなら順番を統一
  • 使わない引数は消してすべての引数を使う
  • 状態変数やエラー変数は最後に配置
  • ルーチン引数はルーチン内で作業用変数として使わない
  • 引数のインターフェース条件を明記する
  • 引数の数はだいたい7個に制限(マジカルナンバーと同じ)
  • 入力、変更、出力引数の命名規則を検討するとよい(先頭にi_,o_とか)
  • ルーチンのインターフェースの抽象化維持のために必要な変数/オブジェクトを渡す(引数をまとめてオブジェクトで渡すのが正しい場合も、変数で渡す方が正しい場合も両方ある)
  • 名前付きの引数を使用する(VB限定、仮引数)
  • 実引数が仮引数と一致することを確認する(型指定で渡す言語以外)

 などなど、メソッドひとつにとっても事細かに解説しています。最後はC++に限定し、マクロルーチンやインラインルーチンを解説しているのも時代を感じさせます。

第8章 防御的プログラミング

 入力がすべて正しいと仮定せず、何か悪いモノが入ってきてもプログラムがクラッシュしたりせず動いたり適切にエラーを処理したりするテクニックの数々。添えられている壊れた橋の絵が生々しいですね。入力チェックやエラー時の処理の分岐、モダンな言語はたいてい備えてる例外の話は基本ですが、これから学ぶ人には躓きやすいところなので重要でしょう。
 ここで8.2節を使ってプログラムの前提をテストできるアサーションのことが解説されています。しかしアサーションってあまり使ってるシーンを見かけないような……(同様の感想をネットで多く見かけますね。)
 他、主にC++でのデバッグツールの活用方法や製品コードに防御的プログラミングを残すかなど。昔はエラー処理のコードを開発中と製品で分けたりしていたんですね。今はここはロガーのレベルをDEBUGからERRORに落とすとかではないかと思います。

第9章 擬似コードによるプログラミング

 ここで紹介されている「疑似コードプログラミングプロセス(Pseudocode Programming Process: PPP)とは、まず頭で考えてルーチンに名前を付けて人間の言葉でコメントをつける。その後にルーチン内の実装をまず人間の言語で論理構造含めコメントで書いて整理。その後に1行1行をコードに変えて書いていくというものです。たぶんプログラミングスキルを持ってる人は手でコードを書きながら並行して頭の中で組み立てているような工程でしょう。
 最近のプログラム入門書ではあまり見かけないような?手法ですが、初心者が頭を整理しながら高品質なコードを書くには良いかもしれません。

第3部 変数

第10章 変数の使用

 のっけから次にあげる一般的なデータ型を幾つ知っているかの確認テストから始まり、全問正解だと実は詐欺師チェッカーになってましたよんというジョークをかましながら今度は変数へ、コードの深く微細な所へ続きます。
 変数は使う場所のなるべく近くで宣言する、変数の寿命(=スコープは)なるべく短くする、繰り返し処理での使い方など、ここもプログラミングの基本ですが重要なポイントを押さえています。

第11章 変数名の力

 『リーダブルコード』などでもお馴染みの変数名の話。この章はCODING HORRORや変数名の良い例・悪い例が豊富に上がっており、学習中の人でも理解しやすいと思います。

  • 覚えやすい良い名前は解決策でなく問題を表す
  • 方法(how)でなくもの(what)を表す
  • 調査によると、平均して10~16文字が一番デバッグに手間がかからない
  • 計算値の修飾(Total, Sumなど)は変数名の最後につける
  • 反意語も一般的なものを使う
  • ループ変数もi,j,kより意味のある名前の方が分かりやすい
  • 状態変数は「何とかflag」より対象を表した方がよい
  • bool型変数は肯定的な変数名にする
  • 命名規則は大事、及び実際の例
  • 単語を省略するときのガイドライン
  • 避けた方がよい名前のガイドライン

 などなど。最近はあまり使わないが昔はハンガリアン記法命名Windowsプログラミングではよく使っていた……などという話はMicrosoftの本らしいですね。
 避けた方がよい変数名で、意味が異なるが似たような名前の変数名の違いを心理的距離」と呼ぶ話は興味深かったです。同じような発音の名前を使わないという話に出てくる会話例もいかにも英語の国らしいですね。

第12章 基本的なデータ型

 こちらも基礎の基礎、データ型の話。変換に注意する、整数の桁溢れや浮動小数点のイコール比較、丸め誤差に注意する、文字は列は定数で定義する……などなど。
 Cの話で一項とって文字列は定数+1の長さで宣言、null初期化、strcpy()は危険だなどと詳しく解説しているあたり、昔はCでめんどくさい文字列周りが原因のエラーも多かっただろうし、だからこそモダンな高級言語は文字列操作を使いやすく進化していったんだろうなあと思います。
 if文でややこしい複雑な評価は、判定結果をあらかじめブール型変数に入れておいて使うとよいという話は言語問わず役に立ちそうですね。本書では列挙型も12か月の値を入れたり積極的に使うように、またユーザ定義型のクラスでデータの型を定義するのもよいと推奨しています。

第13章 特殊なデータ型

 こちらはC/C++VBに限って構造体もうまく使うと良いという話、そしてC/C++限定でポインタの話。2010年代ではあまりお目にかからないシーンですが、さぞかし昔はポインタ周りの苦労が多かったんだろうなあと偲ばせます。他、グローバルデータの扱いには注意せよという話。

第4部 ステートメント

第14章 ストレートなコードの構成

 変数の話が終わると今度はそれを使った1行、ステートメントの話に移ります。処理の順序が重要な文、依存性がある場合はルーチンの引数順にしたりコメントを入れて理解できるようにせよとの話。そして順序が関係なければ上から下に読めるコード、まとまりをグループ化して理解できるようにせよという話。

第15章 条件文の使用

 今度はif文がテーマ。正常系、例外系の順にif-elseに書く、if句の中は空にしない。70年代のある調査では本当にelse句が必要なif文は50-80%だったそうです。
 そして複雑なif-then-elseの単純化について。ブール型を返す内部メソッドで条件を置き換える、一般的なケースを先に処理する、予想外のケースも最後のelseで処理する、可能ならcase文など他の言語機能で置き換える。
 そしてcase文について。アルファベットや数値順/正常ケースを先/出現頻度が高いケースを先に並べるとよい、ケース内の処理は単純に、default句はその他ケースだけやエラーの処理だけにする、break文を忘れない、フォールスルー(breakせず次のケースも実行)するときは誤解のないようコメントを書く。
 僕も過去何回かやらかしましたが、やはりcase文のbreakは誰しもうっかり忘れがちなんですね。

第16章 ループの制御

 本書執筆時はまだforeachがC#にしかなかったようですが、今度はループ文の話。while/for/foreachそれぞれのループを使うべき状況、ループの出口が途中にある場合は目立つようにせよ、ループ制御のテクニック。 本書では無限ループの書き方の標準はwhile(true)とし、for(;;)でもよいとしています。
 前処理と後処理は一つにまとめる、ループ変数は書き換えない、ループが終わったあとでループ変数を使わない、クラッシュを避けるためのC++専用で安全カウンタの話、break文やcontinue文の話、ループ変数など。
 ループの長さは本書では長くても50行、通常は10-20行、そしてネストは3段階を超えると見る人の理解能力が減るので避けよとあります。
 ここもアルゴリズムのキホンですが、言語によってループ仕様が違うところも詳しく掘り下げており、CODING HORRORも多めに、やばいループ文を色々並べています。今も昔もループ周りのバグは多いんだろうなと思います。

第17章 特殊な制御構造

 今度はルーチン内部から複数個所でreturnで返す構造、再帰、懐かしのgoto文の話。
 コードが読みやすくなる場合は複数のreturn文もありだが最小限に、そして入力をチェックしてエラーで返すのは結局はルーチンの先頭でやった方がよいという話。
 再帰も作るアプリケーションの種類によってはあまり出てこない所ですが、本書では迷路脱出プログラムを例に、適切に使えば複雑さの克服の有効なツールになることを示しています。面白いのは、コンピュータ・サイエンス系の教科書などで階乗やフィボナッチ数列などのくだらない問題に再起を使っているのは呆れ果てると一刀両断している下りですね。アメリカでも大学の勉強が実践とかけ離れてるコトはあるようです。
 そして、goto文をめぐる議論は途絶えたと思っているだろうが今も古いコードの中には眠っている...と前置きしたうえで、本書ではかなり詳しく論じています。
 2010年代の今からしたらもうほとんど歴史の勉強チックですが、20世紀のたくさんの議論や失敗があってこそ、今のgoto文が廃止されたモダンな言語群があるのだなあと改めて思います。

第18章 テーブル駆動方式

 本書で述べているテーブル駆動方式とは、例えばキーボードからの入力1文字1文字に応じた処理の分岐が多くif-elseなどのロジックの連鎖が複雑になる場合、[それぞれの文字-対応する処理の組] をテーブルに格納すると判定がシンプルになるよというものです。「テーブル」と言っていますがRDBのテーブル限定ではなく、配列だったり設定ファイルだったり判定用クラスの中の固定データだったりするでしょう。
月の日数や保険料の計算方法、海の上のブイから飛んでくる様々なメッセージを印刷する処理を如何にコード量を減らして実現するか、を例にこのテーブル駆動方式を解説しています。
 工夫するとオブジェクト指向ポリモーフィズムを使った解法よりシンプルで済むこともあったりするのが面白いですね。この方式は機会があったら試してみようかと思いました。

第19章 制御構造の問題

 今度は言語を問わず出てくる制御構造の話。論理評価は0と1でなくbool型のtrue/falseで判定する、複雑な条件式はbool変数やboolを返す関数で単純化する、デシジョンテーブル(決定表)を使う、if文は肯定的な論理式を先に書く、ド・モルガンの定理を適用して論理評価を単純化する、論理式は左から右に評価される、0との比較方法。
 複合文は中カッコ({})をちゃんと書く、ifやwhileの中 の何もしないnull文は目立たせる、C/C++固有のテクニックなど。
 ネストの回避では、本書では3,4レベル以上のネストは使わない方がよいとなっています。ネストを減らすテクニックとしてはif-else文やcase文への置き換え、ポリモーフィズムを使う、状態変数を使う……などしっかりと各手法を解説しています。
 連続、選択、反復からなる構造化プログラミング(Structured Programming)も改めて解説。
 最後の複雑さの話では、複雑さの定量化の手法として、1976年のThomas McCabeという人の論文で提唱された「判定ポイント」を使う方法、ifやwhileなどのキーワードごとに+1、case文の選択肢ごとに+1した値で0-5個なら適切、6-10個なら検討、10個より大きいならルーチン分割の必要あり、とする手法を紹介しています。
 今ならJenkinsとかコードチェックのツール系で出てくる循環的複雑度(サイクロマチック複雑度)も、Wikiを見るとこのThomas McCabeさんが生みの親でしたが、昔から複雑なプログラムにはみな悩まされてきたのですね。

 そんな感じで「コード」コンプリートといいつつ要件定義や設計も解説し、メインのコンストラクションに入るとクラスからルーチン(メソッド)、変数にデータ型にステートメントに制御に……と、各章で徹底的に解説をコンプリート、読者のプログラミングスキル向上も徹底的にコンプリートさせています。
 しかしここでまだ半分。下巻はコードの改良やデバッグ、テスト、リファクタリングとがっつり続くのです……。
【以下、続く】