今になってはじめて『CODE COMPLETE 第2版 上 完全なプログラミングを目指して』を読んでいます。

以下、印象に残った点などを自分なりに解釈してメモ。
まだ読んでいる途中であり、今回は「第2部 高品質なコードの作成」をメインとします。

6章 クラスの作成

ADT(Abstract Data Types)

データとそのデータに作用する操作をまとめたものを ADT というらしい。(聞いたことなかった。)クラス設計の前に ADT を使用することによって、ある程度の抽象度を保った状態でドメインを分析できる。これにより、いきなり実装の詳細から考え始めてしまい、気がついたら使いづらいクラスが出来上がったり、実装がむき出しすぎて変更に対する強度が低いクラスが出来上がってしまったりといったことを早い段階で防げる可能性がある。

ADT の例として原子炉の冷却装置などが挙げられている。

coolingSystem.GetTemperature()
coolingSystem.SetCirculationRate(rate)
coolingSystem.OpenValve(valveNumber)
coolingSystem.CloseValve(valveNumber)

たしかに適度に抽象的であり、実装の詳細までは気にしていない(気にしなくて良い)ことが分かる。

抽象化のレベルには一貫性を持たせる

パブリックインタフェースの抽象度がばらばらで、実際には複数の ADT を表現してしまっているような場合、抽象度の低いインタフェースから破綻していくことが多い。Employees クラスがあったとして、そのパブリックインタフェースに addEmployee と NextItemList が混在しているような状況。後者は「リスト」レベルの抽象化であり実装の詳細に近くなっている。こういうクラスをメンテナンスしていくと、そのコードの抽象度も当然ばらばらになりやすい。コードは既存コードにひきずられて伝染していく。

カプセル化は抽象化よりも強力

抽象化は詳細を無視できるようにする。カプセル化は詳細を見たくても見られないようにする。

書き手より読み手

コードは圧倒的に読むことのほうが多い。したがって、書く時に楽になるようなコードではなく、読むのが楽なコードにすべき。楽というのは、抽象化により詳細に潜っていかなくても何をしているのか理解出来るような状態と理解。

クラスの使い方を理解するために実装を見始めたら赤信号

インタフェースに対するプログラミングではなくなっている。クラスの実装をやたら知っているクライアントコードが出来上がって、変更に対して開いたコードになってしまう。ライブラリの実装を見に行くというのはよくやるが、それはだいたいの場合、使い方を理解するためではないと思うので別の話。

クラスのメンバは7個あたりまで

他の作業を行いながら人が覚えられる項目は、「7 プラスマイナス 2」個ということが研究から分かっているとのこと。それをクラスのメンバ数にそのまま当てはめられるのかは別として、理解しやすいクラスのための目安にはなるかもしれないと思った。

継承のために設計されている場合のみ継承を使用する

継承の目的は、複雑さへの対処なのだから、継承を使用することによってかえって複雑になるのであれば使うべきではない。間違っても思いつきレベルや、将来サブクラスが必要になるかもレベルで使ってはならない。サブクラスがひとつである基底クラスなどは、その時点で継承を使用している意味はない。LSP を意識することは基本。継承を頭から毛嫌いして一切使わない人もよく見るが、継承の危険性から逃げたいならそれも一手といえるかも知れない。ただ、自分はどちらかというと見極める力をちゃんと持ち、正しい選択が出来るようになりたいと思った。

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

上巻で一番ボリュームがある章。

設計はやっかいな問題

解決してはじめて問題を問題として正確に定義できるような問題。設計もこれにあたるとの主張。確かに設計の最適解はあとから分かるシーンはありがち。

設計テクニックはヒューリスティック(発見的)

設計は非決定論的なので、結局、試行錯誤や反復がつきもの。本質的にそういうもの。

ソフトウェアの鉄則

ソフトウェアで最も重要な技術的要素は、複雑さへの対処であるという主張。他の技術目標は二の次とも。かなり強めな主張だが、複雑さへの対処の重要性はこの書籍の中でしつこいくらい何度も、そして章を超えて述べられており、ひとつのテーマになっているようだ。

「凝った」設計

「凝った」設計はわかりづらくなるので、単純で理解しやすい設計にすべきとの主張。「凝った」設計の定義は記されていないのだが、言わんとする事はなんとなくわかった。設計に関連する知識が少ない場合、一般的には「凝っていない」設計であっても「凝った」設計と受け取られるといった誤解はありそう。アーキテクトのレベルによって同じ設計が異なるように見えてしまうような問題が実際に発生することなんてあるんだろか。

高いファンイン・低いファンアウト

ファンインとかファンアウトという言葉での説明ははじめてだったが、関心の方向という概念は他の書籍等でもよく説明されている。ファンアウトの目安としてのクラス個数に、上述した7個というのがここにも出てきていた。ファンアウトの対象をクラスではなくてインタフェースにすれば、さらに良さそう。あるいはそういう場合はファンアウトとは呼ばないのだろうか。

ヒューリスティクス

言葉自体に馴染みがなく、意味も若干あいまいさがあったので理解するのがやや難しかった。最終的には、「それを実践したら良い結果になることが多いよ。」というヒント集のようなものと理解した。これをやったら間違いないといった断定的なものではない。設計におけるヒューリスティクスがいろいろ挙げられていた。

情報隠ぺい

クラスの章で述べられていたカプセル化といった考え方を設計のコンテキストで説明している。情報隠ぺいはいろいろなレベルで効果的であると主張されている。「何を隠ぺいすべきか」と自問する習慣をつけると良い。隠ぺいすべき第一候補は変化しやすい部分だろうなぁと理解している。

不安定な部分の分離

不安定な部分、つまり変化しやすい部分への依存がシステムの最大の敵という主張は他の書籍でもよく見るし、実際そう思う。そのため、不安定な部分を見極めて、適切に分離・カプセル化することが重要になるわけだが、これって結構難しいと思っている。「変わるかもしれない」という根拠のない予想に基づくと、無駄な複雑さを生むだけなのだが、かといって「ほぼ変わることが目に見えている」レベルであれば予想であっても対処すべきだろうと思う。ここの見極めについてはマニュアル化できないのだろうと思う。

変更されやすいものの例として以下のようなものが挙げられていた。

  • 業務ルール
  • ハードウェアへの依存部分
  • 入出力
  • プログラミング言語の非標準機能
  • 設計が本質的に難しい箇所
  • 状態変数
  • etc

依存は見えやすいほうが良い

依存関係は隠されているよりおおっ広げになっているほうが良いという主張。他クラスのメソッドに具象クラスのオブジェクトを引数で渡しているといった依存は分かりやすいが、グローバル変数(オブジェクト)を複数のクラス間で暗黙的に使用しているといった依存はわかりづらいので避けるべき。グローバル変数のデメリットは、いつのまにか変更され得るという点が筆頭というイメージだが、こういうデメリットもあるということ。

セマンティック結合

聞いたことのない用語だったが、そのままの意味だった。密結合は問題と考えられているが、結合の種類のうちセマンティック結合と呼ばれる意味的結合は最も質が悪いという警告。依存オブジェクトの内部実装を知っていないと書けないコードを書いてしまうこと。実装に依存するので実装が変わるとそのまま壊れるし、わかりづらい不具合に結びついたりする。たとえば、「このメソッドは引数で渡すオブジェクトのメンバのうち、これとこれだけを使うから、そこに値をセットしてから引数として渡す。」など。たしかに危険。

クラスやメソッドが単純化に寄与していないなら、それらは目的を果たしていない

これも例の主張の繰り返し。目指すべきは何はともあれ複雑さへの対処だということ。

デザインパターンの落とし穴

よくある話で、以下のような問題。

  • コードを無理にパターンに押し込めようとする
  • パターンを試してみたいという理由でパターンを使用する

主張としてはもっともだが、こういう失敗をしていかないと優秀なアーキテクトがいないような現場で成長していくことはできないのでは?と思う。

設計で常に誤りと言えること

いったい設計はどこまでやれば良いのか?という疑問については、ここまでという回答はやはり出来ないようだが、「常に誤りであると保証できること」は2つあるという。

  • すべての詳細を残らず設計すること
  • 何も設計しないこと

要はバランスという結論になり、そのバランスをとるのが難しいところなのでしょう。

「一生かかっても構造化設計やオブジェクト指向での悟りの境地に達することはない。」

そう言われるといろいろ楽になるし、個人的にはそもそも目指すべきところは悟りの境地やスーパーアーキテクトではなくて、仕様変更や機能追加のときに最小コストかつ楽しんで対応できたり、対応スピードの速さにおいて競合よりアドバンテージを取れるといったところと考えている。

7章 高品質なルーチン

前提としてルーチンというのは、メソッド、関数、プロシージャといったものをまとめて表現しているとのこと。

ルーチン作成の理由は複雑さの低減

また出た。複雑さの低減

1行のコードしかないルーチンにも意味はある

ルーチンの実装コードが1行とか数行になる場合、「切り出す意味はない。」という判断に陥りがちだが、そうではないという主張。単純な除算を行ってその結果を使うだけのコードがあるとする。1行なのでそのまま記述しており、プロジェクト内にそれが10箇所あるとする。ここで、ゼロ除算を制御する必要が出た場合、ルーチン化していれば修正箇所は1箇所で済むが、ルーチン化していない場合は10箇所修正することになる。というような例で説明されていた。なるほど。

凝集度のいろいろな概念

凝集度には以下の種類があるとのこと。

  • 機能的凝集度
  • 情報的凝集度
  • 連絡的凝集度
  • 時間的凝集度
  • 手順的凝集度
  • 論理的凝集度
  • 暗号的凝集度

用語はどうでも良くて、それぞれの概念を理解する必要がある。下に行くほど凝集度が弱くなりやばい。すべてのルーチンが機能的凝集度を持っていたら理想なのだろうが、現場でそのようなプロジェクトは見たことがない。論理的凝集度あたりまではよく目にする気がする。とはいえ、機能的凝集度に可能な限り寄せていく努力はすべきなのだろう。

ルーチンのインタフェースが表す抽象概念とは何か

よくある議論に関する内容。ルーチンの引数を設計する際に、複数のスカラー値とするか、それらをメンバとして持つクラスのインスタンスとするかといった議論である。議論の主眼がカプセル化の維持などに傾倒するとした上で、この書籍ではそうではなく、ルーチンのインタフェースが表す抽象概念とは何かという点を主眼とせよと主張されている。確かに。

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

クラスやルーチンを作成する手段として、疑似プログラミングプロセス(Pseudocode Programming Process : PPP)の紹介がされていた。

擬似コードでは思い込みや概略レベルの誤りがプログラミング言語のコードよりも顕著に現れる

なるほど。この特性は実際のコンストラクション時に大きなメリットになるなぁと思った。早い段階で誤りに気付けるメリットはかなり大きい。

ルーチンがなぜ動いているかの理由まで理解せよ

自分が実装したルーチンなので当たり前といわれるかも知れない。しかし、当たり前のことは意外と忘れやすいのでメモしておく。

ハックに走るのは完全に理解していない所為

ハックして実行、ハックして実行を連続する。たまにやってしまうことがある。たしかにそういうシーンでは、対象の理解が浅いことが多いように思う。分かってないから動かして確認しようとするわけで。そういう状況になったら、手を止めて一回深呼吸したほうが良さそう。

PPP以外の手法

  • TDD
  • リファクタリング
  • 契約による設計
  • ハック

どれも有名と思いきやハックも挙げられていた。この書籍ではハックには否定的だが、ハックをメイン手段とし、それを手段として推奨するプログラマもいるとのこと。体系的なアプローチを採らないでプログラミングしている場合、だいたいハックという形に近くなっているのかもしれない。

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

そうなるはずだと決めつけない

防御的プログラミングの概念の説明。車の運転をたとえとして説明していた。他のドライバーが危険な運転をしたとして、そのドライバーに過失があったとしても、自分の身は自分で守るという考え方とのこと。この考え方をプログラミングに適用したもの。

「ごみ入れ、ごみ出し」はだめ

こうした表現ははじめて聞いた。要するに、不正な入力をしたら不正な動作をするプログラムのことを指すようだ。プロダクトコードがこれだと困るので、「ごみ入れ、何も出さない」「ごみ入れ、エラーメッセージ出し」「ごみ入れ禁止」などを採用することになるよねという話。

アサーション

業務で最も触っているのが PHP であったからか、プログラミング言語に組み込まれているアサーションをまともに使ったことがなかった。PHP7 では、かなり実用的なアサーションが組み込まれたとのことなので、キャッチアップしようと考えている。そもそも、自動化されたユニットテストがある状態で、どの程度アサーションというものを使用すべきなのかもあまり理解していない。

堅牢性と正当性

ソフトウェアの性質の説明。堅牢性は、ソフトウェアの実行を継続できるようにする性質。不正な結果を返す可能性を許容する。正当性は、ソフトウェアが不正な結果を絶対に返さないようにする性質。不正な結果を返すくらいならクラッシュしたほうがマシという立場。医療機器に組み込まれたソフトウェアなどは後者が多そう。

例外は使いようによっては複雑さを軽減できる

例外を継承と同様で、使い方を間違えると複雑さが増すという立場で説明しているのがおもしろい。例外機構というのが、そもそも複雑で大きめな機能になりがちという主張のようだ。蛇足だが、Go 言語が例外機能を提供していないのは、同じような思想に起因していそうである。

例外の抽象化レベルはルーチンのインタフェースの抽象化レベルと一致させる

例外はルーチンから投げられるものなので、ルーチンのインタフェースの一部である。そのため、ルーチンのインタフェースの抽象化が一貫していなければならないことに関連してくる。例外以外のインタフェースが適切に抽象化されていても、例外の抽象化だけがずれている場合、クライントコードがまずい抽象化と結合してしまい、カプセル化が破綻するということ。油断ならない。

まとめ

結論、もっと早く読むべき書籍だったという印象。

ソフトウェア設計や、オブジェクト指向の基本原則や、デザインパターンといったものに関する書籍は、数え切れないほど存在しているが、この書籍はその名のとおり「コード」に帰結している内容であるため、業務に直結しやすいアドバイスとなっていると感じた。他者に対して「これが高品質なコードだ。」と主張することはなかなかできることではないが、この書籍がそれを行っている一方で一定の説得力を感じるのは、とんでもない数の参考文献リストと様々な研究データを根拠としているからなのではなかろうかと思った。

とはいえ、この書籍で主張されている方法論が唯一無二ではないし、いついかなるときでも正しいというものでもないので、自分なりに噛み砕いて消化したうえで、業務やOSS活動での実践を繰り返していくこと(つまり、コードを書きまくること)で、本当に身に付けたと言えるレベルまで持っていきたい。