はじめに
「CODE COMPLETE 第2版 上」を読んだ。ソフトウェア開発で気を付けるべき点が丁寧に解説されていた。当たり前にやっている部分や、ピンとこない部分を読み飛ばしたりしているので、すべてを読んだうえでの感想ではないが、ソフトウェア開発者、特にオブジェクト指向言語を利用して開発している人は、一回は読んどいたほうが良いだろうなと感じた。
設計から、変数名のつけ方やルーチン(Javaでいうとこのメソッド)内での処理の書き方(ループ書く時にはどうするとか)等々、プログラミングに関して抑えるべきところ点を教えてくれる。概念的な話もあるが、実践的な話に重きが置かれている(と思う)ので、読んだ翌日から改善が行える。
学んだこと全部書いていくと、本が一冊かけて印税でウハウハになってしまうので、特に印象的だった部分を、備忘録として残しておこうと思う。
(「オブジェクト指向のこころ(ブログでの記事はこちら)」にも通じるところがあって、セカンドオピニオン的に理解を助けてくれるものにもなっている)
学んだこと
疎結合の考え方
「5.3.7 疎結合の維持」の章に主に述べられている。
設計で目指すのは、他のクラスやルーチンとの関係を小さく、直接的で、可視的で、柔軟なものにすること、つまり「疎結合」である。
自分の経験を踏まえると、疎結合であればあるほど、クラスは変更に強く、使用される側のクラスが変更されても、使用する側のクラスはそれを気にすることなく使用し続けることができる。
疎結合であればコードの変更を行いやすいのに対して、クラス同士が密結合になっていると、一つの修正がどこに影響が出てくるかわからない。
自分が実際に困った例だと、引数にオブジェクトを受けるようになっているが、呼び出し元によっては、一時的に必要なメンバーの情報だけ埋めてオブジェクトを生成して、引数として渡しているようなケースがある。呼び出すメソッドの実装に依存してしまっているので非常に結合度が高くなっており、修正に気を遣う。
本書では、モジュール間の結合を評価する基準として以下の3つが紹介されていた。
サイズ
可視性
柔軟性
サイズ、可視性についてはいまいちピンとこなかったのもあり特に柔軟性の部分を記しておこうと思う。
柔軟性
柔軟性は、モジュール間結合をどれくらい簡単に変更できるかを表す。
…
社員に毎年与えられる休暇日数を、雇用日と職種から割り出すルーチンがあるとしよう。このルーチンの名前をLookupVacationBenefit()とする。それとは別のモジュールに、雇用日と職種を持つemployeeオブジェクトがあり、そのモジュールがemployeeオブジェクトをLookupVacationBenefit()ルーチンに渡すとしよう。
…
このモジュールはemployeeオブジェクトは持たないものの、雇用日と職種を持つ。こうなるとLookupVacationBenefit()ルーチンはとたんに、新しいモジュールとの結び付きに抵抗する扱いにくいモジュールとなる。 3つ目のモジュールがLookupVacationBenefit()ルーチンを使用するためには、employeeクラスについて知る必要がある。フィールドが2つだけのダミーのemployeeクラスを作成することも可能だが、そのためにはLookupVacationBenefit()ルーチンの内部に関する知識が必要になる。
…
他のモジュールからの呼び出しが簡単であればあるほど、そのモジュールの結合は弱い。
柔軟性の例は、自分が実際に出会った困った例と全く同じ状況である。(そして自分が設計するときも特定のパラメータだけを受け取るべきか、オブジェクト全体を受け取るべきか悩むところである)
これについて、一つの考え方が後の章(7.5 ルーチンの引数の仕様)で示されている。
- ルーチンのインターフェースの抽象化を維持するために必要な変数またはオブジェクトを渡す
最初の流派の支持者は、ルーチンに必要な3つの要素だけを渡して、ルーチン間の結びつきを最小限に抑え、結合度を弱め、理解しやすく、再利用しやすいものにすべきだと訴える
…
2つ目の流派の支持者は、オブジェクトを丸ごと渡すべきだと訴える。呼び出されたルーチンが、ルーチンのインターフェースを変更せずにオブジェクトの他のメンバを使用できるくらい柔軟であれば、インターフェースをさらに安定させることができる。
抱えていた問題の状況はまさにこれである。個人的な経験だけで言えば、前者の方が使いやすい場面は多いと思っている。呼び出す側にオブジェクトが用意されていない場合、特定の要素だけを渡せば処理してくれることが明確かつ、Javaに関していえば、引数がプリミティブ型であれば、値が変更される恐れもないので、呼び出す側からするとそちらの方が使いやすいと感じることが多い。
個人的な使いやすさはさておき、考え方として以下のように示されている。
これらの意見はどちらも短絡的で、***ルーチンのインターフェースが表す抽象概念は何か***という最も重要なポイントを見逃している。ルーチンは3つの要素が渡されることを期待していて、それらがたまたま同じオブジェクトによって提供されることが抽象化であるというなら、3つのデータ要素を個別に渡すべきだろう。しかし、常に特定のオブジェクトが存在し、ルーチンがそのオブジェクトを使って何かをすることが抽象化であるというなら、3つのデータ要素を公開した時点で、抽象化を崩壊されることになる。
うーん、なるほど。元々の例でいえば、休暇日数を割り出すルーチンが、「雇用日と職種」から割り出すものことが目的で、結果的にemployeeオブジェクトが持っていただけなのか、「employeeオブジェクト」から割り出すのが目的で、結果的に利用したのが雇用日と職種だったのか、この違いによって、引数として受け取るものを選択しなさいという意味だと理解した。
結合の種類
本を読んで良いことの一つは、なんとなく感覚的にあったものをパターン化して提示してくれるところだと思う。本書でもモジュール間の結合の種類を提示してくれている。
単純データパラメータ結合
2つのモジュール間でやり取りされるデータがすべて基本データ型のみであり、すべてのデータが引数(パラメータ)リストとして渡される場合、2つのモジュール間の結合を「単純データパラメータ結合」と呼ぶ。
int method(int arg1, int arg2) {
…
}
2
3
こういう理解。求める引数が基本データ型になっていて、さらに返り値も基本データ型。モジュール間は、基本データ型でのみつながっている。
単純オブジェクト結合
モジュールがオブジェクトをインスタンス化する場合、モジュールとそのオブジェクトとの結合を「単純オブジェクト結合」と呼ぶ
Module2 module2 = new Module2();
こういう理解。Module1がModule2をインスタンス化した場合、単純オブジェクト結合。
オブジェクトパラメータ結合
Object1がObject2からObject3を受け取ることを要求する場合、2つのモジュール間の結合を「オブジェクトパラメータ結合」と呼ぶ。Object1はObject2がObject3について知っていることを要求するため、Object2に基本データ型だけを渡すように要求する場合よりも、結合は密である。
Object1 object1 = new Object1();
Object3 object3 = createObject3();
int val = object1.method(object3)
2
3
こういう理解。この処理はObject2の処理。Object1のmethodにObject3を引数として渡している。この状態は、Object1の処理を呼び出すにあたって、Object3を用意する必要がある。この場合、基本データ型を渡すだけよりも結合が強くなってしまうということだ。
なぜ結合が強くなるのか自分自身の経験踏まえて考えてみると、Object1のmethodの変更が難しくなるからだと考えられる。最初に述べた例の通りだが、例えば、Object1の処理で新たにObject3の別のメンバーを利用したくなったとする。このとき、呼び出し元が正しくObject3を渡しているかどうかを気にかけなければいけない。本来、インタフェースが正しく利用されていれば、中途半端なオブジェクトが渡されているかを気にかける必要はないのだが、それが保証されていなければ、気にかけざるを得ない。気にすることが増えるということは、それだけ結合が強くなっている証拠といえる。
セマンティック結合
最も油断ならない結合は、あるモジュールが別のモジュールの構文要素ではなく、別のモジュールの内部のしくみに関するセマンティクスを使用する場合に生じる
うーん、セマンティク。「セマンティク」ってどういう意味ってなるので、調べる。
セマンティックとは、一般的には「意味」や「意味論」に関することを指す語である
引用: weblio辞書
この文脈で解釈すれば、呼び出し先の実装に依存し意図をくみ取って実装することが「セマンティック結合」に該当するのだと理解している。
なお、余談だがセマンティックスは名詞で、シンタックスと一緒に使われることが多い。シンタックスは構文を意味する。構文を示すシンタックスに対して、意味を示すセマンティクスなのである。
本書ではいくつかの例が示されている。
以下の見出しは私がつけたので、もしかしたら解釈が誤っていて、見出しの間違っているかもしれない。
制御フラグを渡して何をするか伝える
Module1がModule2に制御フラグを渡して、Module2に何をするか伝える。この方法では、Module1がModule2の内部の仕組み、つまり、Module2がその制御フラグを利用して何をするか推測しなければならない
呼び出し先の処理の中身を把握していないと、どういった制御フラグを渡していいのかわからない。
グローバルデータの使用
Module2は、Module1がModule2の要求する方法でデータを変更し、Module1が適切なタイミングで呼び出されるものと想定している。
JavaScriptのグローバル変数とか、Javaのクラス変数とかが書き換え可能な状態になっていて、その変数を利用して処理を行っているような状況と理解している。
この場合、グローバル変数はいい感じに変更されていて、期待した状態になったうえで処理を呼び出すことを期待している。呼び出し側に期待することが多すぎる。
処理の依存関係の把握
Module2はModule1.Routine()がどのみちModule1.Initialize()を呼び出すことを知っているので、Module1をインスタンス化して、Module1.Initialize()を先に呼び出さずにModule1.Routine()を呼び出すだけにする。
呼び出し先の実装を把握したうえで、何を呼び出すかを決めてしまっている。Module1.Routine()がModule1.Initialize()の呼び出し処理を消してしまったら、途端に影響が出るだろう。実装を依存するのではなくインタフェースに依存(?)しなさいということだと理解している。
仮引数の利用方法の把握
Module1がObjectをModule2に渡す。Module1は、Module2がObjectの7つのメソッドのうち3つしか利用しないことを知っているので3つのメソッドに必要な特定データを使って、Objectの一部だけを初期化する
これもあっちゃいけないけど、ありがちだと思う。自身(Module1)の処理の中にObjectを持っていないときに、Module2が要求するObjectを渡そうとすると、Module2の実装を見たうえで、その処理の中で利用している情報だけを埋めて、いわば不完全なObjectを生成してModule2に渡すのだ。完全なObjectを生成するコストが高い場合にこういう状況が起こりがちだと思う。(DBアクセスが必要とか)
ただ、これをやられると、完全なObjectを受け取るつもりで作っていた処理において変更がとても大変になる。
実引数の実体の把握(呼び出し元への依存)
Module1がBaseObjectをModule2に渡す。Module2は、Module1が実際には、DerivedObjectを渡していることを知っているので、BaseObjectをDerivedObjectにキャストして、DerivedObject用のメソッドを呼び出す
これは今までとは逆で、呼び出し先の処理が呼び出し元の実装に依存しているケースの話だと理解している。インターフェース上、Module2はBaseObjectを受け取ることにしているにも関わらず、Module2の処理の中でDerivedObjectが渡される前提で処理を書いてしまっている状況である。
セマンティック結合の危険性
使用される側のモジュールのコードを変更すると、それを使用する側のモジュールのコードにコンパイル時にはまったく検知されないような問題が発生する
疎結合にするためには、呼び出す処理の中で何が行われているか知らなくても利用できることが大事である。
また、呼び出される側の処理も、呼び出し側の呼び出し方を知らなくても変更できるようになっていなければいけない。
良いカプセル化
「6.2.2 良いカプセル化」の内容が印象的だったので一部抜粋して記しておく。
クラスのユーザについてあれこれ推測しない
クラスは、クラスのインターフェースが示唆する規約に従って設計し、実装すべきである
先ほどの結合の話と大いに関係があるが、インターフェースを実装するに当たっては、その規約のみを意識し、実際のどのクラスから使われるかを意識すべきではない。使い方に制約があるのであればそれは規約として明示しておく必要がある。
カプセル化の意味的な違反には非常に細心の注意を払う
意味的なカプセル化は、構文的なカプセル化と同様に難しい[1]。構文的にはクラスの内部ルーチンとデータをprivateで宣言するだけで、他のクラスの内部のしくみに首を突っ込むことを比較的簡単に避けられる。意味的なカプセル化は、これとはまったく別の問題である。
…
これらの例の問題点は、クライアントコードをクラスのパブリックインターフェースではなく、そのプライべートな実装に依存させていることだ。クラスの使用法を理解するためにクラスの実装を調べていることに気付いたら、それはインターフェースに対するプログラミングではない。インターフェースを***通じて***実装***に対する*** プログラミングをしているのである
…
インターフェースで公開されているドキュメントだけではクラスの使用法がわからない場合は、ソースコードを引っ張り出して実装を調べたりしないことが正しい反応である。調べるという姿勢は評価できるが、判断を誤っている。正しい反応とは、クラスの作者に連絡をとり、「このクラスの使用法がわからない」とはっきりいうことである。そして、そのときのクラスの作者の正しい反応は、あなたの質問に面と向かって答えないことだ。そして、クラスインターフェースのファイルを調べ、クラスインターフェースの仕様書を書き直し、新しいファイルをチェックインして、「これでわかるかどうか見てくれないか」と知らせることである。
これは、まさにさきほどのセマンティック結合に関する話であり、それを避けるための方法が提示されている。
疎結合な作りにするためには、インターフェースを設計する人間に対して疎結合に作る努力が求められるが、その一方で、それを呼び出す側の処理を書く人間にも、疎結合を維持する努力が求められる。
個々人のやり取りで解決するのではなく、ドキュメントとして残すことで将来にも役立つものとして改善することが求められる。
ちなみに原著(Code Complete: A Practical Handbook of Software Construction, Second Edition)には、The difficulty of semantic encapsulation compared to syntactic encapsulation is similar. と記されており、semanticとsyntacticが対に利用されているのがわかる。 ↩︎