読んだ本
おことわり
- 私はPHPerなので、コード例はPHPで書きます。
- 自己解釈の結果なので、元の説明などと相違があったりします。
前提知識
クラスが持つ3つの役割
クラスが持つ3つの役割によると、
- ユーザー定義型(interface)
- 実装の再利用の単位(trait)
- インスタンス生成器(newとかclone)
があるらしい。
「オブジェクト指向における再利用のデザインパターン」では、専ら「クラス」と「インスタンス」に言及しているので、これら3つの役割のうち、一つだけに言及しているパターンもあれば、そうでないものもある。まぁ、第1章の概論では、「インターフェース(ユーザ定義型)に対してプログラミングしろ」等と言っているが、高々意識問題程度の認識だと思う。
第1章 概論
オブジェクト指向設計は簡単なことではなく、"再利用可能な"設計をするのはさらに難しい。
初心者は、(OOMやOOPで)利用可能な豊富な機能に惑わされ、結局、以前から用いられている非OOなテクニックにすがる傾向にある。
設計においては、デジャヴることが多いが、それらに「名前付けし、説明を加え、評価した」ものが、デザインパターン(カタログ)である。当然、そのようなパターンをすべて記せているわけがない。(良くも悪くも)頻出する重要なデザインパターンの幾つかを書籍化し、カタログにしたものである。
以下のパターンについては論じていない。
- 並行性のあるパターン
- 分散プログラミングのパターン
- リアルタイムプログラミングのパターン
- ドメイン依存のアプリケーションパターン
- UIの設計パターン
- DB設計パターン
デザインパターン is なに
デザパタは、以下の構成要素があるらしい。
- パターン名
- そのパターンが解決する問題
- そのパターンが如何にして問題を解決するかの、解法
- 結果発生する、トレードオフ
パターン一覧
パターン名 | 確定タイミング | 使用目的 |
---|---|---|
Abstract Factory | 実行時 | 生成 |
Adapter | コンパイル時 | 構造 |
Bridge | 実行時 | 構造 |
Builder | 実行時 | 生成 |
Chain of Responsibility | 実行時 | 振る舞い |
Command | 実行時 | 振る舞い |
Composite | 実行時 | 構造 |
Decorator | 実行時 | 構造 |
Facade | 実行時 | 構造 |
Factory Method | コンパイル時 | 生成 |
Flyweight | 実行時 | 構造 |
Interpreter | コンパイル時 | 振る舞い |
Iterator | 実行時 | 振る舞い |
Mediator | 実行時 | 振る舞い |
Memento | 実行時 | 振る舞い |
Observer | 実行時 | 振る舞い |
Prototype | 実行時 | 生成 |
Proxy | 実行時 | 構造 |
Singleton | 実行時 | 生成 |
State | 実行時 | 振る舞い |
Strategy | 実行時 | 振る舞い |
Template Method | コンパイル時 | 振る舞い |
Visitor | 実行時 | 振る舞い |
「確定タイミング」について
書籍では、「構造」という項目で「クラス」と「オブジェクト」となっていた。それぞれ、「コンパイル時」と「実行時」に当たる。
確定タイミングは、そのパターンに関連するクラス・インスタンスの型が確定するタイミング。
- コンパイル時に確定するパターンは、**型(インターフェース)に注目する。クラス指向**なオブジェクト指向のパターン。
- 実行時に確定するパターンは、メッセージングに注目する。**メッセージ指向**なオブジェクト指向のパターン。
「使用目的」について
- 生成に関するパターンは、**「インスタンスをどのように生成するか(Construct)」**を扱う。この場合の「生成」は、インスタンス生成器(Constructor)の責任のことを言っている。振る舞いに関するパターンの一部。
- 構造に関するパターンは、**「クラスやインスタンスの構成」**を扱う。クラス図やコンポジット図などで表現される。
- 振る舞いに関するパターンは、**「クラスやインスタンス間のメッセージング方法」**を扱う。シーケンス図やアクティビティ図などで表現される。
デザインパターンで設計問題を解く
以下の手順で設計~実装を行う。
- オブジェクトを見つける
- オブジェクトの粒度を決める
- オブジェクトのインターフェースを決める
- オブジェクトの実装を決める
オブジェクトを見つける
OOPは、オブジェクト(インスタンス)から構成される。オブジェクトは、データとそのデータを操作するメソッドをパッケージ化する。
オブジェクトは、クライアントからのメッセージを受け付けた時に、オペレーションを実行する。メッセージは、インスタンスに対してオペレーションを実行させる唯一の方法。オペレーションは、オブジェクトの内部データを変更する唯一の方法。この制約のある状態を、カプセル化されている状態という。
- カプセル化
- カプセル粒度
- オブジェクト同士の依存関係
- オブジェクトの柔軟性
- オブジェクトの性能
- オブジェクトの進化の可能性
- オブジェクトの再利用性
オブジェクトの粒度は、後からいくらでもリファクタできる
オブジェクトのサイズは、リファクタリングするために「再設計」は必要だが、「再実装」は必要ではない事が多いので、ユースケースを満たす必要十分なサイズになっていれば良いと思う。(Adaptorパターンの乱用。)
オブジェクトのインターフェースを決める
1つのメソッド(オブジェクトのオペレーション)は、「名前」「引数」「戻り値」の3つに定義される。(この3つは、オペレーションのシグネチャという。)この、シグネチャの集まりをインターフェースという。インターフェースを表現するために使われる、インターフェースの名前を、型(Type)という。
インターフェースがスーパータイプのインターフェースすべてを有する時、そのインターフェースの型はそのスーパータイプのサブタイプといい、継承しているという。
メッセージングされた際、レシーバとなるオブジェクトのインターフェースがメッセージを実行できることが保証されていれば、実行時まで実装を確定しなくても良い。この時のメッセージとその実装の関係を動的束縛という。動的束縛を用いて実行時にオブジェクトを置き換えることが可能な特徴のことを、ポリモフィズムという。
- クラスの構造
- クラスの振る舞い
- オブジェクトの構造
- オブジェクトの振る舞い
オブジェクトの実装を決める
オブジェクトは、クラスをインスタンス化することによって生成された、インスタンスである。
- クラスの継承とインスタンスの継承の区別
- インターフェースに対してプログラミングせよ
- クライアントは、扱うオブジェクトが求めるインターフェースに従っている限り、そのオブジェクトの型を知る必要は無い。
- **クライアントは、**これらオブジェクトの実装をしているクラスを知らなくて良い。インターフェースのみ知っていれば、使用できる。
インターフェースに対してプログラミングするのであってじそすに対してプログラミングするのではない。
再利用の仕方
継承とコンポジション
機能を再利用する方法には、「継承」と「(オブジェクトの)コンポジション」のの2つがある。それぞれ、その可視性から、「ホワイトボックス再利用」と「ブラックボックス再利用」という。
--- | 継承 | コンポジション |
---|---|---|
可視性 | protectedとpublic | publicだけ |
解決タイミング | コンパイル時(静的) | 実行時(動的) |
結合度 | 少なくともインターフェースを共有するので、高い | インターフェースを合わせる必要もないので、低い |
クラス継承よりも、オブジェクトコンポジションを多用せよ
らしい。
ただし、「多用せよ」であって、「乱用せよ」ではない。例えばTemplate Methodなら継承の一択だし、Bridgeならコンポジションになる。使い方を誤ってはいけない。
委譲(コンポジション)
委譲を主としているパターン
- State
- Strategy
- Visitor
一部で委譲しているもの
- Mediator
- Chain of Responsibility
- Bridge
クラス継承とパラメータ化した型
(ただし、これを活用したパターンは紹介されていない。)
クラス継承使うな(個人的見解)
私は、クラスが持つ3つの役割では「型を継承させずに実装を継承させたい→それ移譲で」という発言もあることから、私は次のように理解をしている。
- 継承とは、専らインターフェースを継承することである。
- 委譲とは、専ら実装を継承することである。
クラスなんて使うな、interfaceとtraitに分離しろ
って感じである。
クラス継承を活用したデザパタは紹介されていなかったわけだし。クラス継承なんてなかったんだ、と思った。
デザインパターンを用いたリファクタリング
アンチパターン的な状態を挙げ、リファクタリングをするのにどのパターンが使いやすいかを示す。
基本的には、「実装に依存している」状態から、「インターフェースへ依存している」状態へ変更することになる。
最初から汎用性・拡張性・自由度を高める必要はない。YAGNIすべきなので、特定のデザインパターンの適用が必要になるまでは、デザインパターンを適用すべきではない。リファクタリングする余地があろうとも、必要になるまではリファクタリングしない。
「クラスを明確に決め、オブジェクトの生成している」
オブジェクトの生成をする際に、クラス名を定めることは、特定の実装に依存していることになる。変更コストが非常に高く、場合によっては変更が困難なこともある。
- Abstract Factory
- Factory Method
- Prototype
「特定のオペレーションに依存している」
特定のオペレーションを決める際に、ユースケースを満たすために必要最低限の実装だけしている。これは、コンパイル時に確定してしまうが、実行時にも変更できるようにする。
- Chain of Responsibility
- Command
「ハードウェアやソフトウェアプラットフォームへの依存している」
特定のプラットフォームに依存したている。移植するのが辛いだけでなく、プラットフォームのバージョン上げすら難しくなる。
- Abstract Factory
- Bridge
「オブジェクトの表現や実装へ依存している」
クライアントがオブジェクトの内部情報や実装に依存している場合、オブジェクトを変更した時に、クライアントも変更しなければならない。
- Abstract Factory
- Bridge
- Memento
- Proxy
「アルゴリズムへの依存」
アルゴリズムを局所化する。
- Builder
- Iteratro
- Strategy
- Template Method
- Visitor
「密結合」
複数のクラスが密結合になっているは、変更箇所が複数箇所に分散していることに同義である。
- Abstract Factory
- Bridge
- Chain of Responsibility
- Command
- Facade
- Mediator
- Observer
「サブクラスを用いて機能拡張している」
簡単な拡張をするためだけに、サブクラスを導入するのはコストが重たい。
- Bridge
- Chain of Responsibility
- Composite
- Decorator
- Observer
- Strategy
「簡単なクラス変更が不可能」
「クラス感の密結合が過ぎ、プラットフォームにも依存し…」みたいな場合、そもそもクラスへの変更が難しい場合もある。そういうときは、ハリボテなインターフェースを作って、そっちに依存させ直す。
- Adapter
- Decorator
- Visitor
デザインパターンカタログ
当該書籍にあるような説明や概要は、ストラテジックチョイスさんとかTECHSCOREさんとかを見ればわかる。ここでは、私が自己解釈した結果を書いていく。
Singletonパターン
Singletonパターンは、特別視して良いと思う。Singletonは、以下の様な状態に限り、利用すべきであるかを検討する。
Singletonパターンはどのようなときに使うのか?という記事を書いたので、そちらを見て欲しい。
Flywieght
Flyweightパターンは、特別視して良いと思う。利用状況が限られているし、メンバ変数にpoolを要するなど、"デザイン(設計)パターンのなかでは、比較的"実装寄り"に思う。
私は、無駄にSingleton化されたオブジェクト群を非Singleton化する際に、Flyweightパターンを用いて管理させている。Singletonによる弊害が顕著化するのは、「そのSingletonが状態を持っていて、変化させ始めた時」がほとんどだと思う。そういう場合は、状態ごとにハッシュ化し、インスタンスをFlyweightにPoolさせておくのが良い。
生成に関するパターン
生成に関するパターンは、振る舞いに関するパターンの1派である。
生成に関するパターンは、クラスの「インスタンス生成器の機能」をブラックボックス化し、クライアントから隠蔽する。
生成に関するは、以下の順番で、抽象度が増加していく。
- Abstract Factory
- Factory Method
- Builder
- Prototype
Abstract Factoryは、単に、パーツクラスのインスタンス生成器のエイリアスである。
Factory Methodは、AbstractFactoryではそれぞれ別のメソッド名だったものを、同じメソッド名にし、生成するインスタンスの種類の依存性を注入できるようにする。
Builderは、Factory Methodで作成したパーツインスタンスを合成し、1つのコンポーネントのインスタンスを作れるようにする。
Prototypeは、Builderで作った後に状態が変化したインスタンスを、cloneできるようにする。
これらのパターンは、抽象度を増しているだけで、"関連"はあるが構造上・振る舞い上の"依存性"はない。
私はこの「生成に関するパータン」を、まとめて「Manufacture」と呼んでいる。
構造に関するパターン
構造に関するパターンは、主に以下の3つのパターンに集約される。
- Aggregator: 代わりに何かする
- Composite: 木構造を利用する
- Mix-in: 多重継承のような機能を提供する
表にすると、次のようになる。
Aggregator | Composite | Mix-in | 使用用途 | |
---|---|---|---|---|
Adaptor | ○ | リファクタ用 | ||
Bridge | ○ | ○ | 実装の隠蔽用 | |
Composite | ○ | 木構造 | ||
Decorator | ○ | ○ | フィルタリング用 | |
Facade | ○ | たくさんの処理をまとめておく | ||
Proxy | ○ | ○ | LaravelのHTTPミドルウェアみたいなやつ |
構造だけで見るとと似てるようでも、用途まで視野に入れて実装方法まで考慮すると、明確な区別ができる。
振る舞いに関するパターン
振る舞いに関するパターンは、それぞれのパターンが、各ユースケースに合わせたインターフェースと実装の両方を、ある程度示しているので、特にまとめたりできないと思う。各言語で、具体的にどのように実装できて、どのように特化可能かをそれぞれに考えておけばいいと思う。
ただし、以下の2つに関しては、ただ使用用途に対して名前をつけただけに過ぎず、パターン名として名前を消費するだけの価値が有るのか疑問だったりする。
- Mediatorはあまりにも抽象度が高すぎて、他のパターンに対しても「これ、順次処理する手順を隠蔽してるだけだからww(Interpreter)」など言える。この単語が出てきたら、「あ、こいつ実装のこと考えてないな」と警戒したほうが良いかもしれない。
- Template Method パターンは、ただ単にInterfaceの「型の定義は、メソッドシグネチャの強制に相当する」特徴に名前をつけただけにすぎない。 「Template Methodにしよう」などと言うくらいなら、具体的なメソッド名を示したほうが親切だ。
デザインパターンに何を期待するか
共通の設計用語
共通の設計用語
(中略)
デザインパターンを用いることで、より高いレベルで設計し、設計について議論することが可能になるのだ。
本書のデザインパターンを身につければ、設計に関する用語の諸説はほぼ確実に変わるだろう。デザインパターンの名前によって直接話すことができるようになるのだ。そして、自然と、“ここではObserverパターンを使おう"、あるいは“これらのクラスからStrategyを作ろう"などと言うようになるだろう。
文書化と習得の促進
まぁ、人間の強みの一つである学習は、「文書化し、他者に学習する機会を残す」ことで、促進されるしな。
全体への感想
『オブジェクト指向における再利用のデザインパターン』のカタログにあるパターンは、そのままでも使えるが、特に振る舞いに関するパターンは、組み合わせることによる恩恵が大きいと思う。例えば、「トランザクション管理ができて、UndoやRedoができる実装を提供するTrait」とか。Proxyパターンでの実装方法を工夫すれば、アスペクト指向のようなこともできる。
オブジェクト指向をより深く理解するとともに、実用する上で必要な、「オブジェクト指向の応用の仕方」の基礎を学ぶことにも繋がると思う。
とりま、OOP初学者は必読にしてもいいんじゃね?っていうレベルの書籍だとは思う。オブジェクト指向設計の再入門のタイミングですべきだと思う。