マーチン・ファウラーの『リファクタリング』の続編のような本で、デザインパターンを活用しながら、どのようにソフトウェアの設計を改善しけばよいかを示しています。 パターンの知識を付けるだけでなく、パターンの賢い使い方を知ることをテーマとしています。
この本の特徴として、現実のコード、あるいは実際に使用したコードを元にしたコードが使用されている点が挙げられます。 実際のプロジェクトにはリファクタリングに関して多くの制約があり、それは作り物のコードでは体験できないものです。
以下は全11章のメモです。
第1章: 本書を執筆した理由
- コードを必要以上に柔軟にしたり洗練させることは、作り込みすぎ (over-engineering) である。チームのプログラマたち(特に新しく参加した人たち)は、無意味に大きく複雑なコードベースを扱わなければならなくなる。作り込みすぎは生産性を低下させる。作り込みすぎな設計を引き継ぐ場合、拡張や保守を行うのに多大な時間がかかる。
- とはいうものの、作り込み不足 (under-engineering) は作り込みすぎよりずっと多い。
- TDD と継続的なリファクタリングのリズムを身に着けるには経験と時間が必要だが、この開発スタイルに慣れてしまえば、別の方法で実稼働するコードを作成することは奇妙で、不安で、プロフェッショナルらしくないと感じるようになる。
- 優れた設計を行いたいなら、設計そのものを調べるよりも、その設計がどのように進化してきたかを知らべる方が有益だ。真の知恵は進化の中に存在する。
第2章: リファクタリング
- コードが明確でない臭いの元は、リファクタリングで取り除くべきであって、コメントで脱臭するべきではない。そのようなコードをリファクタリングをするときは、そのコードをよく理解している人に立ち会ってもらうのが一番である。
- 実際にリファクタリングを促すものは感情だ。私はコーディングの不快感を少しでも減らすためだけにリファクタリングを行うことがよくある。
- マーチンファウラーの言葉「コンパイラが理解できるコードは誰にでも書ける。すぐれたプログラマは、人間にとってわかりやすいコードを書く。」
- 小さい単純なステップに分けることで、大きなステップよりも間違いなく早く目標にたどり着くことができる。
- アプリチームとフレームワークチームを1つのチームにしておけば、それらがちぐはぐになることがない。フレームワークはアプリのニーズにもとづいて作られるので、価値のあるフレームワークのコードだけが作成される。ただし、このプロセスには継続的なリファクタリングが必須である。それによって、フレームワークとアプリを分けておくことができる。
第3章: パターン
- パターン魔 (patterns happy) は、パターンに魅了され、コードでパターンを使わずにいられなくなった人。誰しもがパターンを学ぶ過程でパターン魔になる。リファクタリングによってパターンを徐々にシステムに組み込んでいくようにすれば、パターンによって作り込みすぎる可能性は低くなる。
- 残念なことに多くプログラマは、デザインパターン本に例示されている各パターンの構造の図が、そのパターンを実装する唯一の方法だと間違えて捉えている。『デザインパターン』の共著者の一人であるジョン・ブリシデスも、「実際のコードにはいろいろなニーズや制約があり、示されている構造の図とは大きく異なってくる」と述べている。例示されている構造をそのまま実装するのではなく、パターンの実装を必要最小限に抑えることは、進化的設計のプラクティスである。目的は設計をよりよくすることであって、パターンを実装することではないことを忘れないこと。
- 一般的には、パターンを実装することで、コードの重複を取り除き、ロジックを単純化し、意図を伝えやすくし、柔軟性を高めることができるはずである。しかし、パターンに慣れていない人がコードを読むと、わかりにくい、複雑すぎると感じることがある。このような意見の食い違いが発生した場合は、パターンの使用をやめるより、チームがパターンを学ぶ方がよい。
第4章: コードの臭い
- もっともよくある設計の問題は、次のようなコードが原因である。
- 重複している
- 不明確である
- 複雑である
- メソッドの適した行数はどのくらいであろうか?ほとんどのメソッドが1~5行のコードからできているものが適切だと私は考えている。小さなメソッドを連鎖させても、性能はほとんど低下しない。プロファイラを使えば明らかである。
第5章: パターンを取り入れるリファクタリングのカタログ
この章では、この本の読み進め方が述べられています。
第6章: 生成
Creation Method によるコンストラクタの置き換え (Replace Constructors with Creation Methods)
- 問題: 1つのクラスに複数のコンストラクタがあると、開発時にどのコンストラクタを使うかの判断が難しくなる。
- 対策: コンストラクタの代わりに、意図がわかりやすい Creation Method を作成し、それがオブジェクトのインスタンスを返すようにする。
- 利点:
- どのような種類のインスタンスが返されるかがコンストラクトよりもよく伝わる。
- 引数の数と型が同じであるコンストラクタを2つ作成できないといった、コンストラクタの制限事項を回避できる。
- 使われていない生成コードを見つけるのが簡単になる。
- 欠点: 生成方法が標準に準拠しなくなる。new によってインスタンスを生成するクラスと「Creation Method」を使うクラスとが混在する。
Factory による生成処理の置き換え (Move Creation Knowledge to Factory)
- 問題: クラスのインスタンス化に使うデータやコードが数多くのクラスに散在している。
- 対策: 生成に関する知識を1つの Factory クラスに移動する。
- 利点:
- 生成ロジックとインスタンス化/設定のための情報をまとめられる。
- クライアントを生成ロジックから切り離すことができる。
- 欠点: 直接のインスタンス化に比べて設計が複雑になる。
Factory によるクラス群の隠蔽 (Encapsulate Classes with Factory)
- 問題: 1つのパッケージ内に存在して共通のインタフェースを実装しているクラス群を、クライアントが直接インスタンス化している。
- 対策: クラスのコンストラクタをパブリックでなくし、クライアントには Factory 経由でインスタンスを生成させる。
- 利点:
- さまざまな種類のインスタンスの生成を、意図が明確なメソッド経由で行うことで、単純化できる。
- 公開する必要のないクラスが隠蔽されるため、パッケージの「概念的重み」(by Bloch) を減らすことができる。
- 「インタフェースに対してプログラミングするのであって、実装に対してプログラミングするのではない」(書籍『デザインパターン』より)という原理を厳しく適用できる。
- 欠点
- 新しい種類のインスタンスを生成しなければならない場合には、Creation Method の新規作成や変更が必要になる。
- Factory のソースコードではなくバイナリコードにしかアクセスできない場合には、カスタマイズが制限される。
Factory Method によるポリモーフィックな生成の導入 (Introduce Polymorphic Creation with Factory Method)
- 問題: 階層内のクラスが、オブジェクトの生成ステップを除いて同じようにメソッドを実装している。
- 対策: そういったメソッドをスーパークラスで1つにまとめ、そこで Factory Method を呼び出してインスタンス化の処理を行う。
- 利点:
- オブジェクトを生成するステップが異なることが原因で生じている重複が減る。
- どこで生成が行われているか、どのようにオーバーライドされているかが効果的に伝えられる。
- Factory Method で使うためにクラスがどの型を実装しなければならないのかが明確になる。
- 欠点: Factory Method を実装するクラスに不必要な引数を渡さなければならないことがある。
Builder による Composite の隠蔽 (Encapsulate Composite with Builder)
- 問題: Composite の構築処理が何度も出現したり、複雑であったり、あるいはエラーを起こしやすいものになっている。
- 対策: 詳細部分を Builder に任せることで、構築を単純化する。
- 利点:
- Composite を構築するクライアントコードを単純化できる。
- Compsoite の生成にまつわる繰り返しやエラーを軽減できる。
- クライアントと Composite の間の結合度が低くなる。
- カプセル化された Composite や複合オブジェクトを異なった形式で表現できる。
- 欠点: インタフェースの意図が伝わりにくくなる可能性がある。
Singleton のインライン化
- 問題: コードからあるオブジェクトにアクセスしなければならないが、グローバルなアクセス方法は必要でない。
- 対策: Singleton の機能を1つのクラスに移し、そのクラスにオブジェクトを格納してアクセス手段を提供する。Singleton は削除する。
- 利点:
- オブジェクトの協調関係がより見えやすく明示的になる。
- 唯一のインスタンスを保護するための特別なコードを必要としない。
- 欠点: いくつもの層を経由してオブジェクトインスタンスを渡すのが面倒だったり困難だったりする場合には、設計が複雑になる。
第7章: 単純化
メソッドの構造化 (Compose Method)
- 問題: メソッドのロジックをすぐに理解できない。
- 対策: 意図の伝わりやすい、詳細レベルが揃った小さなステップ群にロジックを変換する。
- 利点:
- メソッドが何をし、それをどのように行うかが効果的に伝わる。
- 詳細レベルが揃った、わかりやすい名前がついた振る舞いに分割することで、メソッドを単純化できる。
- 欠点:
- 小さなメソッドが増えすぎることがある。
- 多数の小さなメソッドにロジックが分散するため、デバッグが困難になることがある。
Strategy による条件判断の置き換え (Replace Conditional Logic with Strategy)
- 問題: いくつかの計算方法のうちどれを実行するかを、メソッド内の条件ロジックで制御している。
- 対策: 計算方法ごとに Strategy を作成し、元のメソッドは計算処理を Strategy のインスタンスに委譲する。
- 利点:
- 条件ロジックが減る、あるいは取り除かれるため、アルゴリズムが明白になる。
- アルゴリズムのバリエーションをクラス階層に移すため、個々のクラスが単純になる。
- 実行時にアルゴリズムを別のものに置き換えることができる。
- 欠点:
- 継承による解決策や「条件記述の単純化」のリファクタリングを使った方が簡単な場合には、それよりも設計が複雑になる。
- アルゴリズムがコンテキストクラスとデータをやり取りする方法が複雑になる。
Decorator による拡張機能の書き換え (Move Embellishment to Decorator)
- 問題: コードがクラスの核となる責務に対する拡張機能を提供している。
- 対策: 拡張機能を Decorator に移動する。
- 利点:
- 拡張機能が取り除かれるのでクラスを単純にできる。
- クラスの核となる責務と拡張機能とを効果的に区別できる。
- 関連する複数のクラスに含まれる重複した拡張ロジックを取り除くことができる。
- 欠点:
- 装飾対象のオブジェクトと装飾後のオブジェクトは異なるものになってしまう。
- コードを理解したりデバッグしたりするのが困難な場合がある。
- Decorator を組み合わせた際、互いに悪影響を及ぼす場合には、設計が複雑になる。
State による状態変化のための条件判断の置き換え (Replace State-Altering Conditionals with State)
- 問題: オブジェクトの状態遷移を制御する条件式が複雑である。
- 対策: 条件式ではなく、個々の状態とその間の遷移を扱う State クラスを使う。
- 利点:
- 状態を変えるための条件ロジックがなくなる、あるいは減る。
- 状態を変える複雑なロジックが単純になる。
- 状態を変えるロジックを俯瞰することができる。
- 欠点: 状態遷移ロジックがもともとわかりやすい場合には、設計が複雑になるだけである。
Composite による暗黙的なツリー構造の置き換え (Replace Implicit Tree with Composite)
- 問題: String などの基本データ型の表現によって、暗黙的なツリー構造を作っている。
- 対策: 基本データ型の表現を Composite で置き換える。
- 利点:
- ノードの形成、追加、削除といった手順の繰り返しをカプセル化できる。
- 同じようなロジックの増殖に対処する汎用的な方法となる。
- クライアントの構築作業が簡単になる。
- 欠点: 暗黙的なツリー構造を作成するほうが簡単な場合には、設計が複雑になるだけである。
Command による条件付きディスパッチャの置き換え (Replace Conditional Dispatcher with Command)
- 問題: 条件ロジックによってリクエストを振り分け、アクションを実行している。
- 対策: アクションごとに Command を作成する。Command をコレクションに格納し、条件ロジックを Command を取り出して実行するコードに置き換える。
- 利点:
- 一律に同じやり方で、さまざまな振る舞いを実行するためのシンプルなメカニズムである。
- どのリクエストをどのように処理するかを実行時に変更できる。
- 実装するためのコードが少ししか必要でない。
- 欠点: 条件付きディスパッチャで用が足りる場合には、設計が複雑になるだけである。
第8章: 汎用化
Template Method の形成 (Form Template Method)
- 問題: 複数のサブクラスの2つのメソッドが、同じ順番で似たようなステップを実行しているが、それらのステップはまったく同じではない。
- 対策: 各ステップを同じシグニチャを持つメソッド群に抽出してメソッドを汎用化し、それから汎用メソッドを引き上げて Template Method を形成する。
- 利点:
- 不変な振る舞いをスーパークラスに移すことで、サブクラス間の重複したコードを取り除くことができる。
- 汎用のアルゴリズムのステップを簡潔にし、効果的に伝えることができる。
- サブクラスで簡単にアルゴリズムをカスタマイズできるようになる。
- 欠点: アルゴリズムを肉付けするためにサブクラスで多くのメソッドを実装しなければならない場合には、設計が複雑になる。
Composite の抽出 (Extract Composite)
- 問題: 階層内のサブクラスが同じ Composite を実装している。
- 対策: Composite を実装するスーパークラスを抽出する。
- 利点:
- 子に関する格納と処理の両ロジックの重複をなくすことができる。
- 子を処理するロジックを継承することが効果的に伝わる。
- 欠点: 特になし。
Composite による単数・複数別の処理の置き換え (Replace One/Many Distinctions with Composite)
- 問題: あるクラスが、1つのオブジェクトの場合と複数のオブジェクトの場合とを別のコードで処理している。
- 対策: Composite を使って、1つのコードで、1つのオブジェクトの場合と複数オブジェクトの場合との両方を処理できるようにする。
- 利点:
- 単数または複数のオブジェクト処理に関するコードの重複を取り除く。
- 単数または複数のオブジェクトを統一したやり方で処理できる。
- 複数オブジェクトの処理機能が豊富になる(OR表現など)。
- 欠点: Composite の構築時にタイプセーフかどうかの実行時チェックが必要なことがある。
Observer によるハードコードされた通知の置き換え (Replace Hard-Coded Notifications with Observer)
- 問題: 別のクラスの1つのインスタンスに対する通知がサブクラスにハードコーディングされている。
- 対策: Observer インタフェースを実装した任意のクラスの任意の数のインスタンスにスーパークラスが通知を送れるようにし、サブクラスを削除する。
- 利点:
- 観察対象と観察者の間の結合度が低くなる。
- 観察者が単数の場合にも複数の場合にも対処できる。
- 欠点:
- ハードコーディングされた通知で用が足りる場合には、設計が複雑になる。
- 通知がカスケードしている場合には、設計が複雑になる。
- 観察対象から観察者が削除されないと、メモリリークが起きる可能性がある。
Adapter によるインタフェースの統合 (Unify Interfaces with Adapter)
- 問題: クライアントが2つのクラスと相互作用していて、その1つが好ましいインタフェースを持っている。
- 対策: Adapter によってインタフェースを統合する。
- 利点:
- クライアントコードが同じインタフェースを通じて複数のクラスとやり取りできるため、コードの重複をなくしたり減らしたりできる。
- 共通のインタフェースを通じてオブジェクトやり取りできるため、クライアントコードが簡潔になる。
- クライアントが複数のクラスとやり取りする方法を統合できる。
- 欠点: アダプタを作らなくてもクラスのインタフェースを変更できる場合には、設計が複雑になるだけである。
Adapter の抽出 (Extract Adapter)
- 問題: 1つのクラスが、コンポーネント、ライブラリ、API、あるいは他のエンティティの複数バージョンに対するアダプタになっている。
- 対策: コンポーネント、ライブラリ、API、あるいは他のエンティティのバージョンごとに Adapter を1つ抽出する。
- 利点:
- コンポーネントやライブラリや API のバージョンごとの違いを切り分けることができる。
- クラスの責務を1つのバージョンに対応することだけに限定できる。
- 頻繁なコードの変更が必要な部分を限定できる。
- 欠点: Adapter で提供されていない重要な振る舞いをクライアントが使えなくなることがある。
Interpreter による暗黙的な言語処理の置き換え (Replace Implicit Language with Interpreter)
ある言語の文法に関して、実装する必要のあるクラスが10程度までなら、Interpreter パターンを使ってモデリングするのが有効かもしれない。
- 問題: 1つのクラスの数多くのメソッドが組み合わさって暗黙的な言語処理 (Implicit Language) の要素を表している。
- 対策: 暗黙的な言語処理の要素を表す複数のクラスを定義し、そのインスタンスを組み合わせて解釈可能な表現を形成する。
- 利点:
- 言語要素の組み合わせを、暗黙的な言語よりもうまくサポートできる。
- 言語要素の新しい組み合わせをサポートするのにコードを追加する必要がない。
- 振る舞いを実行時に設定できる。
- 欠点:
- 文法を定義し、それを使うようクライアントコードを変更するという初期コストが伴う。
- 言語が複雑な場合には膨大なプログラミング必要になる。
- 言語が単純な場合には設計が複雑になるだけである。
第9章: 保護
クラスによるタイプコードの置き換え (Replace Type Code with Class)
- 問題: フィールドの型(String や int など)によっては、安全でない値を代入できてしまったり、正しい等値比較ができなかったりする。
- 対策: そのフィールドの型をクラスにして、代入や等値比較を制約する。
- 利点: 無効な代入や比較に対する保護が向上する。
- 欠点: タイプセーフでない型を使う場合よりコードの量が増える。
このリファクタリングをするには、列挙型を使うほうが向いていると思うかもしれない。 クラスと列挙型の違いは、クラスには振る舞いを追加できるということ。 第7章の「State による状態変化のための条件判断の置き換え」の場合と同様に、一連のリファクタリングを適用する中で、クラスに振る舞いを付け足していく必要があるかもしれないので、列挙型ではなくクラスによってタイプコードを置き換えることに強みがある。
Singleton によるインスタンス化の制限 (Limit Instantiation with Singleton)
- 問題: コードがオブジェクトのインスタンスを複数生成しており、それによってメモリーの使いすぎやシステムの性能低下が発生している。
- 対策: 複数のインスタンスを Singleton で置き換える。
- 利点: 性能が改善される。
- 欠点:
- どこからでもアクセスしやすい。これは設計の問題を示していることが多い。第6章「Singleton のインライン化」を参照。
- オブジェクトに共有できない状態が含まれている場合には有効でない。
ヌルオブジェクトの導入 (Introduce Null Object)
- 問題: ヌルのフィールドや変数を扱う同じロジックが、コードのあちこちにいくつも存在する。
- 対策: ヌルロジックを、何もないときの適切な振る舞いを提供するヌルオブジェクト (Null Object) に置き換える。
- 利点:
- ヌルロジックを何度も記述しなくてもヌルエラーを防止できる。
- ヌルかどうかのチェックを最小限に抑えられるため、コードがシンプルになる。
- 欠点:
- ヌルかどうかのチェックがシステム内でほとんど必要でない場合には、設計が複雑になるだけである。
- ヌルオブジェクトが実装されていることをプログラマが知らなければ、ヌルかどうかのチェックが重複して行われてしまう。
- 保守が複雑になる。スーパークラスを持つヌルオブジェクトは、新しく継承したパブリックメソッドをすべてオーバーライドしなければならない。
第10章: 累積処理
Collecting Parameter による累積処理の書き換え (Move Accumulation to Collecting Parameter)
- 問題: 1つの巨大なメソッドの中で、ローカル変数に情報を累積している。
- 対策: 抽出したメソッドに Collecting Parameter を渡して、そこに結果を累積する。
- 利点: 巨大なメソッドを、小さくシンプルで読みやすいメソッド群に変換できる。
- 欠点: できあがったコードの実行速度を上げることができる。
例えば、1つの StringBuilder をパラメータ (Collecting Parameter) としてメソッドに渡していき、1つの文字列を構築する。 巡回されるメソッドは、それぞれが Collecting Parameter に情報を提供する。 関連するメソッドすべての巡回が済んだ後、累積した情報を Collecting Parameter から取得することができる。
Visitor による累積処理の書き換え (Move Accumulation to Visitor)
- 問題: 1つのメソッドが種類の異なるクラス群から情報を累積している。
- 対策: 累積処理を Visitor に移動し、Visitor が各クラスを巡回して情報を累積する。
- 利点:
- 種類の異なる1つのオブジェクト構造に対して数多くのアルゴリズムを適用させることができる。
- 同じ階層のクラスでも異なる階層のクラスでも巡回することができる。
- 型キャストをしなくても、種類の異なるクラスの型固有のメソッドを呼び出すことができる。
- 欠点:
- 共通のインタフェースによって種類の異なるクラスを同じように扱える場合には、設計が複雑になる。
- 巡回対象のクラスを追加すると、新しい受け入れメソッドと、各 Visitor 上の新しい巡回メソッドが必要になる。
- 巡回対象クラスのカプセル化が破られることがある。
Visitor パターンの特徴はダブルディスパッチによって Visitor のメソッドが呼び出されるところ。
- 巡回対象の各要素をポリモーフィックにループで呼び出せるようにするため、各要素が Visitor を受け取る共通の accept メソッドを用意する(1つ目のディスパッチャ)。
- そして、各 accept メソッドの中で、自分自身(要素)の型に合った Visitor のメソッド(例: visitHogeElement)を this をパラメータにして呼び出す(2つ目のディスパッチャ)。
Collecting Parameter パターン vs Visitor パターン
Visitor の場合、巡回先のオブジェクト (visitor object) が Visitor インスタンスに自分自身を渡すのに対し、Collecting Paramter では、巡回先のオブジェクトは、ただ Collecting Paramter のメソッドを呼び出して情報を提供する。
大抵のコードは Collecting Paramter による累積処理の書き換えで十分であり、Visitor による累積処理の書き換えが必要になることはほとんどない。 作成する Visitor が巡回しなければならないクラスの集合が、今後も頻繁に増えるのであれば、一般には Visitor を使うのを避けたほうが賢明である。
種類の異なるオブジェクト(つまり、異なるインタフェースを持つオブジェクト)から多種多様な情報を集める場合には、Collecting Paramter よりも Visitor を使ったほうが、おそらくすっきりとした設計になる。
第11章: ユーティリティ
(この章には大したことが書かれていないので省略)