第5章はInformation Hiding (and Leakage)
というタイトル。いいモジュールを作るには情報の隠蔽(カプセル化)を行うことが大事だよ、という話。
5.1 Information hiding
- Deep Moduleを作る上で最も大事なのは情報の隠蔽
- モジュールを実装する上で必要なメカニズムをinterfaceとして表には出さないこと
- ex)
- B-treeにどういう情報を保存するか
- ファイルの中身が物理的なディスクのブロックのどこに存在するかを認識する方法
- TCPプロトコルの実装の詳細
- マルチコアCPUでのスレッドの実装
- JSONをどうやってパースしているか
- ex)
- 情報の隠蔽は2つの方法で複雑性を減らす
- 情報の隠蔽はモジュールのインターフェースをシンプルにする
- 具体例) B-tree classを開発者が使うときに、理想的なノードの広がりやツリー内部の均衡化については、利用者は気にしなくて良い
- 情報の隠蔽はシステムの発展を容易にする
- 情報がモジュール外には隠蔽されている=(モジュール外からみて)その情報への依存性がなくなっているので、その情報に関する内部的な設計の変更の影響はモジュール内部のみである
- 例えば、TCPでネットワーク混雑時の制御についてプロトコルの仕様が新しく追加されても、TCPのデータの送受信のコードには影響がないはずである
- 情報がモジュール外には隠蔽されている=(モジュール外からみて)その情報への依存性がなくなっているので、その情報に関する内部的な設計の変更の影響はモジュール内部のみである
- 情報の隠蔽はモジュールのインターフェースをシンプルにする
5.2 Information leakage
- 直訳すると情報の漏洩。情報の隠蔽の反対の概念
- これはモジュール間の依存を作り出してしまう
- 情報の漏洩はインターフェースだけで起こるものではない
- 例えば特定のファイルフォーマットを扱う2つのクラスがあり、片方がファイルを読み込み、もう片方が書き込みをするような場合。ファイルのフォーマットが変更された時は両方のクラスを修正する必要がある。
- 情報の漏洩もまたred flag(危険信号)
- 情報の漏洩をどうやって防ぐか?
- 先ほどの2つのクラスの例であれば、クラスが小さくて密接に関連しているのであればマージして1つにしてしまうのが良い
- 情報をpull(取得)してカプセル化するクラスを新しく作る、という方法もある。
- Another possible approach is to pull the information out of all of the affected classes and create a new class that encapsulates just that information. However, this approach will be effective only if you can find a simple interface that abstracts away from the details; if the new class exposes most of the knowledge through its interface, then it won’t provide much value (you’ve simply replaced back-door leakage with leakage through an interface).
5.3 Temporal decomposition
- 時間による分解
- Temporal decompositionの考え方で「ファイルを読み込んで、その内容を変更して、新しいバージョンを書き出す」というアプリケーションを考えると、3つのクラスに分解される。
- ファイルを読むクラスと新しいバージョンを書き出すクラスはそれぞれファイルのフォーマットを知っている必要があるので、ここで情報の漏洩が発生する
- 情報の漏洩を防ぐには、ファイルを読むクラスと書き込むクラスは同じものにする必要がある
- 時間による処理の順番でクラスを分解するのではなく、そのクラスが行う処理が知識として何を持っているべきか(先程の例だとファイルのフォーマット)に着目してクラスを分解するべき
5.4 Example: HTTP server
- 生徒がHTTPプロトコルの実装をした際のデザインの意思決定について、実際の例を交えて考察
- HTTPプロトコル説明の図
5.5 Example: too many classes
- 生徒がよくやってしまう間違いとしては、たくさんのShallow Classに分割してそのクラス間での情報漏洩をもたらしてしまうこと
- 例) ネットワークコネクションからHTTPリクエストを読み込んでstringに変換するクラスを作り、別のクラスでそのstringをパースする
- Temporal Decompositionのダメな例
- 例えばContent-Length headerでbodyの長さが指定されているので、Bodyの長さを計算するにはまずContent-Lengthをパースしないといけない
- stringになったリクエストをパースする時にBodyの長さがわからないとパースできない
- コードの具体例がなかったのでちょっとここはよくわからなかった
- 結果として、2つのクラスに分割してもそれぞれがHTTPリクエストの構造を知っていないとダメな実装になっている
- さらに呼び出し側も2つのクラスの使い方を知っている必要が出てくるので、呼び出し側に余計な認知負荷がかかる
- よって、上の例ではリクエストを読み込むクラスとパースするクラスは一つにマージした方が良い
- HTTPリクエストをパースという処理を1クラスに集約することで情報の漏洩が発生しない
- クラスの数を減らすことで利用者から見たインターフェースはシンプルになる
- 例) ネットワークコネクションからHTTPリクエストを読み込んでstringに変換するクラスを作り、別のクラスでそのstringをパースする
5.6 Example: HTTP parameter handling
- HTTP requestがサーバーに来ると、サーバーはリクエストの情報を参照する必要がある
- 例えば、以前のFigure 5.1で言うとphoto_idというリクエストパラメーター
- パラメーターはURLのQuery Stringとして送られてくることもあれば、ボディで送られてくることもある
- Query Stringの場合はURLEncodeされているが、処理する時にはこれはデコードされている必要がある
- 著者の生徒の大半は以下の良い情報の隠蔽を行なっていた
- パラメータはQuery StringまたはBody経由で送信されてくるが、それを利用する人にはどちらから来ているかは重要ではないのでマージしている
- 2つ目の良いところは、パラメーターのデコードについて利用者が意識しなくて済むようにしていること
- ただし、生徒が作るモジュールは以下の点でShallow Moduleだった
- HTTPRequestというクラスを作ってその中に
Map<String, String> getParams()
というメソッドを定義していた
- HTTPRequestというクラスを作ってその中に
public Map<String, String> getParams() { return this.params; }
Map<String, String>
という内部のデータ型を利用者に公開するのではなく、String getParam(String name)
という形にした方が、仮に内部のデータ構造を変えたいときに利用者に影響を与えずに済む- パフォーマンスチューニングのために内部のデータ構造を変更することもあるので、なるべく内部のデータ構造は利用者からは見えないようにした方が良い
- あとはこのMapを利用者側で変更できないようにImmutableにする必要があるなどの問題もある
5.7 Example: defaults in HTTP responses
- HTTP Responseに関する、生徒たちがよくやる不適切なデフォルト値について説明
- HTTP ResponseのHTTPのバージョンに関して
- (生徒の)あるチームはメソッドの呼び出し元にHTTPのバージョンを指定させるようにしていた
- しかし、HTTPのバージョンはリクエストで指定されているものと一致しているべき
- なので、リクエストのHTTPバージョンをそのままレスポンスのバージョンとすることが望ましい
- 呼び出し元がHTTPのバージョンを指定すると、このHTTPのライブラリと呼び出し元の間で情報の漏洩が発生してしまう
- HTTPレスポンスに必要なものとしてDateヘッダーもある
- これもHTTPライブラリが気の利いたデフォルト値を提供すると良い
- もし呼び出し元が何かしらの理由でDateヘッダーをオーバーライドしたい場合は、専用のメソッドを用意する
- クラス or モジュールはできる限りいい感じのデフォルト値を用意する
- P.26のJavaのIOバッファリングは、この点でよくない例となっている
- IOライブラリのほとんどのユーザーはバッファリングするのでそれがデフォルトになっているべきだが、Javaの標準ライブラリではバッファリングのために別のクラスを使ってラップする必要がある
FileInputStream fileStream = new FileInputStream(fileName); BufferedInputStream bufferedStream = new BuffferedInputStream(fileStream);
- Red Flag: Overexposure(過度の露出)
- よく使われるAPIが、たまにしか使われない機能もユーザーに覚えさせるようにしてしまうと、認知負荷を増大させてしまう(意味がよくわからなかった)
5.10 Conclusion
- 情報の隠蔽とDeep Modulesは密接に関連している
- モジュールがたくさんの情報を隠蔽することで、インタフェースをシンプルにしつつ多くの機能が提供される
- システムをモジュールに分解するときに、実行時に行われる処理の順番を意識しないようにすること
- 以前に説明したTemporal decompositionが発生し情報の漏洩やShallow modulesの問題を引き起こす
- モジュールが知っているべき知識を考慮してそれを各モジュールがカプセル化することで、Deep modulesを作ることができる