A Philosophy of Software Design ch9 Better Together Or Better Apart?
- Better Together Or Better Apart?
- Bring together if information is shared
- Bring together if it will simplify the interface
- Bring together to eliminte duplication
- Separate general-purpose and special-purpose code
- Example: insertion cursor and selection
- Example: separate class for logging
- Example: editor undo mechanism
- Splitting and joining methods
- Conclusion
Better Together Or Better Apart?
- ソフトウェア設計において一番肝心な疑問
- 2つの機能片があったとして...
- 同じ場所に一緒に実装すべきか?
- 分けて実装すべきか?
- あらゆるレベルのモジュールについて生じる疑問
- 関数
- メソッド
- クラス
- サービス
- 例
- 迷ったら目標に立ち返る
- システム全体の複雑性を減らすこと
- モジュラリティを向上させること
- 細分すればするほど個々の部品はシンプルになるが、システム全体としての複雑性は増してしまう
- 部品の個数が多いことからくる複雑性
- 追いづらい
- インタフェース増える
- 【補】認知の負荷増加
- 多数のコンポーネントを制御しなければならなくなる
- 1つのオブジェクトを利用していた部分が複数オブジェクトを制御するようになる
- もともと1つのメソッド等にまとまっていたコードが遠く離れる
- 別クラス
- 別ファイル
- それらの部品が独立しているなら結構
- モジュラリティの向上
- 依存があるなら良くない
- 見えづらい依存があるとなお悪い
- 【補】back-door dependency
- バグのもと
- コード重複をまねく
- 部品の個数が多いことからくる複雑性
- 密接に関連しているものはまとめるが吉、関連のないものは分割せよ
- コード間に関連がある兆候
Bring together if information is shared
- 例: HTTPリクエストの読み込みとパース
- ボディの読み込みのためにContnet-Lengthヘッダを読まないといけない
- ので、結局ボディの読み込みにはHTTPリクエストのパースの知識が必要
- この場合、同じ場所にまとめたほうが良い
Bring together if it will simplify the interface
- 複数のモジュールを組み合わせて単一のモジュールにすることで、シンプルかつ簡単に利用できるようになるケース
- 元のモジュールが問題を部分的にしか解決しない場合におこる
- 例: JavaのストリームIO
- FileInputStreamとBufferedInputStream
- BufferedInputStreamはFileInputStreamと組み合わせて使用する
- まとめてFileInputSreamとして、デフォルトでバッファリングをサポートするほうがシンプル
Bring together to eliminte duplication
- 同じようなスニペットが何度も何度も繰り返されている場合
Separate general-purpose and special-purpose code
- 例: Textクラス(汎用)とUIクラス(用途特化)
- 汎用のメカニズムを分離する
- 汎用に用途特化を混ぜない
- Textクラスに
backspace
メソッド等を生やさない
- Textクラスに
- 汎用に別の汎用を混ぜない
- 汎用に用途特化を混ぜない
- 汎用は下の層、用途特化は上の層に
- 【補】differenct layer, different abstraction
Red Flag: Speial-General Mixture
- 混ざっていると複雑になり、知識の漏出もおきる
- 例: TextとUI
Text.backspace()
が生えているということは、汎用であるべきTextクラスにUIの知識が漏れている- UIを変更するとTextにも変更が必要になる
- 例: TextとUI
Example: insertion cursor and selection
- GUIエディタの例
- UI
- カーソル
- 常に見える
- 選択範囲
- 何も選択されていないときは非表示
- カーソル
- カーソルと選択範囲には関連性がある
- カーソルは必ず選択範囲の開始・終了地点どちらかの端にある
- UI
- 実装案1: 選択範囲の一方の端をカーソルとして扱い、カーソルクラスと選択範囲クラスを統合する
- カーソルはどちらの端か、booleanメンバで保持
- 1つのオブジェクトにまとまったはいいが、使いづらいものとなった
- 利用側コードは結局選択範囲とカーソルとを区別しなければならない
- 別々に管理するよりも複雑
- カーソルを直接利用するはずの部分はbooleanによる間接指定に
- 選択範囲とカーソルとは、統合できるほど関連深くなかったのである
- 実装案2: 汎用のPositionクラスを追加し、カーソルと選択範囲を表現する
Cursor
-->Position
Selection
-->Pair<Position, Position>
- Positionはカーソルや選択範囲以外にも利用できる、低レイヤの汎用モジュール
Example: separate class for logging
- 一箇所でしか使われない、特定用途の浅いメソッドを大量に生やしてしまう
- logRpcSendError
- logRpcReceiveError
- コードを追う際、呼び出しと宣言を行き来することになる
- 呼び出し箇所にインラインで記述したほうがシンプル
- 行き来しなくてすみ、読みやすい
- インタフェースの単純化
Example: editor undo mechanism
- 【補】長いので抜粋、読み解いてクラス図を起こした
@startuml package UI{ class UI class UndoableCursor class UndoableSelection class UndoableDelete } package History{ class History{ public void addAction(Action action) public void addFence() public void undo() public void redo() } interface Action{ public void redo() public void undo() } } package Text{ class Text { public void insert(Position position, String string) public void delete(Position begin, Position end) public void redo() public void undo() } } UndoableCursor ..|> Action UndoableSelection ..|> Action UndoableDelete ..|> Action UI --> Text History o-- Action UI --> History UI ..> UndoableCursor : <<create>> UI ..> UndoableSelection : <<create>> UI ..> UndoableDelete : <<create>> UndoableDelete --> Text @enduml
- UIは上位レイヤの用途特化モジュール
- Textは下位レイヤの汎用モジュール
- HistoryとAction Interfaceはアンドゥ・リドゥの枠組みを提供する下位レイヤ
- 特定のアンドゥ・リドゥのしかたや、アンドゥをまとめるポリシーは上位のUIレイヤの仕事
- TextクラスがText自身のアンドゥ・リドゥ機能を実装するのはアリ
Splitting and joining methods
- クラスだけではなくメソッドについても、いつ細分化するかの課題はついてまわる
- 「長い」というだけの理由で分割されがち
- 長いメソッドは理解しづらい傾向にあるため
- 「20行以下に分割せよ!」
- 総じて分割しすぎる傾向にある
- 分割すればするほどインタフェースが増えて複雑になる
- システム全体の複雑性が下がらない限り、分割すべきではないのである
- 細分化しすぎると、コードを追うためにあちこち行き来しなければならない
- 長くても、シグネチャが単純で理解しやすければ問題ない
- メソッドを設計するうえで最も重要なのは、きれいで単純な抽象化を提供すること
- これらさえ満たせば長さは重要ではない:
- 各メソッドは一つのことを完全に行うべき
- インタフェースは実装よりも単純であるべき
- メソッドを分割するときはこれらを満たすこと
- これらさえ満たせば長さは重要ではない:
- メソッドを分割するときは(b)が一番良い。ついで(c)がよい。(d)は避けよ
- (b) サブタスクを抽出する
- 呼び出し元 -> 親メソッド -> 子メソッド(サブタスク)
- 親メソッドは、元のメソッドと同じインタフェースをもつ
- 子メソッドが綺麗に分割できる場合に有効
- 効能
- 子メソッドの実装を読む人は、親メソッドのことは知らなくて良い
- 親メソッドの実装を読む人は、子メソッドの実装のことは知らなくて良い
- 子メソッドは下位レイヤーの汎用モジュール
- 他の親メソッドからも共通で利用可能
- Red Flag: Conjoined Methods
- コードを追うにあたり、親メソッドと子メソッドの実装を行き来しなければならないようなら、この分割はまずい
- (c) 別々のメソッドに分ける
- 呼び出し元 -> メソッドA, 呼び出し元 -> メソッドB
- 元のメソッドのインタフェースが過度に複雑な場合に有効
- 元のメソッドが複数のことをやろうとしている
- 分割後は片方だけ呼び出せばよくなるのが理想
- 元のメソッドが複数のことをやろうとしている
- 分割後のメソッドが元のメソッドよりも汎用的になるのは良い兆候
- うまくいくことは少ない
- 呼び出し側は複数のメソッドを呼び出さねばならなくなる
- 状態変数を引き回さねばならないようなら良くない
- (b) サブタスクを抽出する
- 浅いメソッド群を1つの深いメソッドに統合することで、システムがシンプルになることもある
Conclusion
- モジュールの分割/統合の判断は、複雑性に基づいて下す
- 下記のような構造を選定せよ
- 知識をもっともよく隠蔽し
- 依存がもっとも少なく
- インタフェースが単純でモジュールが最も深くなる