【読書メモ4】Java言語で学ぶデザインパターン入門第3版
結城浩, 『Java言語で学ぶデザインパターン入門第3版』, SBクリエイティブ を読む。
C++と共通する部分があるとはいえJavaも個別に学んでおきたいのと、同時にデザインパターンも学べて一石二鳥なため。
本エントリでは概要と自作のUMLクラス図を配置して一覧性を高め、演習に使ったリポジトリを GitHub - U-Ar/design_pattern: 演習 of 『Java言語で学ぶデザインパターン入門第3版』 に置く。
デザインパターンに慣れる
1.Iterator
並んだアイテムに繰り返し処理を行いたいときに用いるパターン。
リストに相当するクラスはIterable
メリット
繰り返し操作が、実装の詳細に依存せずに行える。つまり、hasNextとnextというメソッドさえ持っていればよい。 配列の内部実装を変更しても、利用する側のコードに変更はない。
これはインタフェースが存在する意義といえるだろう。
2.Adapter
継承もしくは委譲を用いて、元のクラスの機能を必要な形に変換するパターン。
(継承の場合)
必要な機能を持つクラスを継承し、それと別に必要なインタフェースを実装する。
(委譲の場合)
Javaではクラスの継承は1つのみであることに注意(インタフェースは複数可能)。Targetがインタフェースでなくクラスで実装されていて継承-実装ができない場合、Adapteeはフィールドにインスタンス化してそれに実行を委譲、Targetのみ継承する、という形で実装する。
メリット
既存のコンポーネントを高速に再利用できる。後方互換性の維持にも役立つことがある。
サブクラスに任せる
3.Template Method
スーパークラスで処理の枠組みを定め、サブクラスでその具体的内容を定めるデザインパターン。
メリット
共通のロジックを一か所にまとめて記述できる。サブクラスの実装時には、スーパークラスとして取り扱っても可能な限り問題なく動作する必要があるのに注意。
4.Factory Method
Template Methodパターンをインスタンス生成の場面に適用したもの。インスタンスを生成する工場(Factory)のイメージ。生成に際して例えば製品の登録などの処理を組み込める。
を実装する。
他にもインスタンスを生成する静的メソッドをstatic Factory Methodと呼ぶことがある
メリット
newによる実際のインスタンス生成をインスタンス生成のためのメソッド呼び出しに変えることで、具体的なクラス名による束縛からスーパークラスを解放する
これによりnew Productの記述がなくなり、Productを抽象クラスのままにしておくことができる
インスタンスを作る
5.Singleton
インスタンスがただ一つしか存在しないことをプログラムで表現するためのパターン。
コンストラクタはprivateになっており、Singletonクラス外部からの生成を禁止している。
静的メンバにインスタンスを保持しておき、getInstanceではそのインスタンスを返すことで唯一性を保証する。
インスタンスは初めてのgetInstance呼び出し時に生成されることに注意。
enumの要素はインスタンスの唯一性が保証されているので、Singletonとして利用することができる。
6.Prototype
インスタンスを、クラス名を指定することなくコピー生成するためのパターン。
使うケースは主に
要は、java.lang.Cloneableインタフェースのcloneメソッドを使いたいが、cloneはprotectedで外部から使えないため、Cloneableを継承したクラスの別メソッドを経由するやり方。
注意点
- Cloneable自体にはcloneメソッドの宣言はなく、むしろjava.lang.Objectで定義されている。あくまでcloneをサポートしていることのマーカーインタフェースでしかない
- cloneは浅いコピー
- cloneを使うよりコピーコンストラクタやコピーファクトリを使う方が良いケースも多い
7.Builder
構造を持ったインスタンスを段階的に組み上げるためのパターン。
MainクラスはDirectorクラスのconstructメソッドだけを呼び出す。DirectorクラスはBuilderクラスのメソッドだけを使って構築する。それ以外の知識を持たないことによりコンポーネントが独立し交換可能性が高まる。 使われる具体的なクラスは後で指定できる(依存性の注入、Dependency Injection)。もちろん設計時に将来的に必要になるメソッドは十分に見通しておく必要がある。
8.Abstract Factory
インタフェースが定まっている抽象的な部品を組み合わせて複雑なインスタンスを組み上げるためのパターン。
Builderパターンは似ているが、段階を追って大きなインスタンスを作る感じ。
分けて考える
9.Bridge
機能を表すクラス階層と実装を表すクラス階層を分け、橋渡しをするためのパターン。
- 機能を表すクラス階層における継承の関係は
- スーパークラスは基本的な機能を持っている
- サブクラスで新しい機能を追加する
- 実装を表すクラス階層における継承の関係は
これらを明示的に弁別する。機能側クラスのフィールドとして実装クラスのインスタンスを保持し、実行を委譲する。
10.Strategy
使用するアルゴリズムを切り替え、同じ問題を別の方法で解くのを容易にするためのパターン。
以前C++で実装した自作の簡潔ビットベクトル( https://github.com/U-Ar/BV )にこのパターンを暗黙的に使っていた。配列全体を定数長のブロックに分割し、ブロック内で立っているビット数に応じて2種のサブクラスに場合分けを行って索引を構築する仕組みになっている。どちらのサブクラスでもrank, selectといったメンバ関数をオーバーライドしており、共通して実行することができる。
メリット
- 委譲という継承よりも緩やかな結合を利用しているので、アルゴリズムを容易に切り替えられる。ユーザとの対戦を行うプログラムなどで有効。
- 実行中に切り替えることも可能。
同一視
11.Composite
容器と中身を同一視し、再帰的な構造を作るためのパターン。木によく使われる。というか改めてCompositeパターンと言われる以前から当然のように使い倒している。
12.Decorator
オブジェクトに修飾(加工)を何度も施していくためのパターン。再帰構造を表現するのが目的のCompositeとは異なり中心の具象クラスに機能を追加していくことが目的。
思いつくところではHTTPサーバインスタンスにクッキー認証などのミドルウェアをかぶせていく利用先があるか。
メリット
- 飾り枠と中身が同一視されているため、APIが全て透過的に取り扱える(Decoratorでラップしても同じメソッドを呼び出せる)
- 中身を変更せずに機能追加ができる
- 動的に機能が追加できる
構造を渡り歩く
13.Visitor
データ構造と処理を分離し、構造を渡り歩く訪問者クラスに処理を任せるためのパターン。データ構造側ではacceptメソッドを実装して訪問者を受け入れる。
ConcreteVisitorはConcreteElementとは別に独立して開発できる。デザインパターン全体の目的として、Open-Closed Principle、拡張に対しては開かれており、修正に対しては閉じられているような部品としての再利用性が高いクラスを目指しており、Visitorもその一つ。
14.Chain of Responsibility
決定する処理を行うクラスをたらい回しで決定し、処理を要求する側と処理する側を分離するためのパターン。
複数のオブジェクトを連結リストで保持し、リンクを渡り歩きながら適切なオブジェクト上で処理が実行される。
処理は一つの分岐で処理先を決定するよりも遅くなるが、プログラムが設計しやすくなることとのトレードオフと言える。
シンプルにする
15.Facade
(Facadeクラスが複数のクラスを参照して処理を行うだけなのでUMLクラス図は省略)
複雑な処理をまとめて高レベルのAPIを作り、単一の窓口を提供するためのパターン。
このパターンがやっていることは単純で、要はユーザコードの手前に一つレイヤを挟み、ユーザコードから分かりやすく使えるAPIを提供するだけ。自分の分野でいえば圧縮接尾辞配列と木のBP表現とLCP配列をまとめて圧縮接尾辞木の機能を提供するクラスがFacadeに相当するだろう。 私の実装したbr-index( https://github.com/U-Ar/br-index )もFacadeパターンと言えるといえば言える。
16.Mediator
多数のオブジェクトの動作を強調する必要があるとき、調停者(mediator)役と各オブジェクト間の通信のみ許すことで単純化するためのパターン。一方向にコントロールするFacadeと違って双方向の通信を行う。
GUIプログラミングにおける表示のコントロールロジックなどに使い道がある。(表示制御はmediator内でのみ取り扱う)
注意点
- 通信経路の組み合わせ爆発を抑える効果がある(インスタンス数に対して線形で済む)
- ConcreteMediator役はアプリケーション依存性の高い部分を詰め込むため再利用しづらい
状態を管理する
17.Observer
観察対象の状態変化を観察者に通知するためのパターン。観察対象の側から通知用のメソッドを呼び出すため、ObserverというよりNotify-Subscribeの関係といった方が直感的。
18.Memento
クラスのカプセル化を破壊することなく状態履歴の保存と復元を行うためのパターン。
全履歴を管理できるかと思って期待したが、インスタンスの数までしか無理そう。
Memento=記念品。状態のスナップショットを記念品と比喩
19.State
状態をクラスで表現するためのパターン。
Stateインタフェースで宣言するメソッドは、状態に依存して変化する処理。本来はクラスメソッド内で状態に応じてifで処理を分岐するが、それをクラス分割の段階で行っている。
注意点
- 処理の記述漏れが実行段階(if文)より前のコンパイル段階(クラスメソッドの実装)で分かる。
- 状態遷移を管理するクラスをどれにするかには注意。
- ConcreteStateが他のConcreteStateへの遷移を管理する形式だと、他の状態に遷移する条件が1つのクラスにまとまっていて分かりやすいが、他ConcreteStateへの依存性が生じてしまう。
- Context役が担当する場合、Mediator的な管理ができるが、Contextが全ConcreteStateを知らなければならない。
- 状態遷移をテーブルを利用して有限状態機械的に表現することもできる。
20.Flyweight
普通に扱うとメモリ消費や構築時間が大きいインスタンスをできるだけ共有して、無駄にnewしないためのパターン。
Flyweight=ボクシングのフライ級。プログラムを軽量化する意図を表している
注意点
21.Proxy
遅延評価を行い、必要なタイミングで初めてインスタンス生成を行うためのパターン。
遅延評価を行う/行わないクラスを個別に作ることで部品化が進み、また利用時の選択も明確に行える。
22.Command
一つの命令(Command)あるいはイベントを、命令を表すクラスのインスタンスとして表現するためのパターン。
23.Interpreter
文法規則をクラスで表現し、DSLのインタプリタを書くためのパターン。
本書の演習問題は全部解いたが、Interpreterの改良問題は特に歯ごたえがあった。( design_pattern/10_ExpressByClass/23_Interpreter_ex at main · U-Ar/design_pattern · GitHub )
総括
JavaではGCにより参照の取り扱いが単純であることから、パターンの本質に集中しやすかった。C++だとunique_ptrを間に挟む必要があったりして煩雑かつ読みづらいコードになる。
今までは古い印象もあってJavaを敬遠していたが、書いているととことんオブジェクト指向原理主義という方針が一貫したある意味純粋な言語で、それゆえに洗練されていてむしろ新しい印象へと改まった。
C++のような「すべてを包含していてどんな書き方もできる言語」よりも、書き方のパターンを原則に従って指示してくるJavaのような言語の方が好みかもしれない。標準ライブラリが極めて充実しており、これ標準装備されてないかなと思ってタイプしてみたメソッドが大抵存在していることも高評価ポイント。