オブジェクト指向における再利用のためのデザインパターン改訂版 読書ノート 2/3¶
- 著者:
Eric Gamma, Richard Helm, Ralph Johnson, John Vlissides
- 監訳者:
本位田真一、吉田和樹
- 出版社:
ソフトバンク クリエイティブ株式会社
- 発行年:
1999 年
- ISBN:
978-4-7973-1112-9
第 3 章 生成に関するパターン¶
<クラス継承よりもオブジェクトコンポジションに頼る形でシステムを発展させていく場合に、生成に関するパターンは重要になる> (p. 89)
MazeGame::CreateMaze
についての考察が数ページ続くが、高密度な記述ゆえに上手い形にノートにまとめられない。2 つの部屋からなる簡単な迷路を作っているだけなのに、コードが複雑 (p. 92)
迷路構成を変更しようとすると、メンバ関数のオーバーライド(実質再定義)か、それと同等の仕事が必要となる (p. 92)
<より柔軟な設計(必ずしも、コードを短くするわけではない)> (p. 93) このカッコ内がポイント。
<インスタンス化されるクラスがコード中に直接書かれていることが最大の問題> (p. 93)
Abstract Factory¶
Motif とか Presentation Manager とかって何?
<各種の基本ウィジェットを生成するためのインタフェースを宣言した抽象クラス
WidgetFactory
を定義する> (p. 95)各種のウィジェットに対する抽象クラスを作成したら、 <その具象クラスで特定の look-and-feel 規格のもとでの実装を与える> (p. 95)
<たとえば Motif ではスクロールバーはボタンやテキストエディタとともに使わなければならないといった制約が、
MotifWidgetFactory
クラスを利用する結果として自動的に規定されることになる> (p. 96)このパターンでは Client は
AbstractFactory
とAbstractProduct
で宣言されたインタフェースのみを利用する (p. 97)<普通、
ConcreteFactory
クラスのインスタンスは実行時に生成される> (p. 97) とあるが、実行時に生成されないインスタンスなど考えられる?新たな種類の部品への対応は
AbstractFactory
とそのすべてのサブクラスについて、インタフェースの修正が必要となる。これが面倒。部品を実際に生成するのは
ConcreteProduct
クラスになるが、各部品について factory method を定義する方法がよく用いられる (p. 98)Prototype パターンを使って
ConcreteFactory
クラスを実装する方法がある。部品の集合が多数存在する場合にそうすることができる (p. 98)<クラスをオブジェクトとして扱うことのできる言語では、prototype を用いたアプローチに変化をつけることが可能になる> (p. 99)
生成する部品の種類を表すパラメータを取る AbstractFactory の手法は、<C++ を使うときには、すべてのオブジェクトが同じ抽象基底クラスを持つ場合か、要求を出すクライアントにより部品オブジェクトが正しい型に変換できる場合にのみ、適用することができる> (p. 100)
この条件はそんなにきつくない。<サブクラスに特有のオペレーション> (p. 100) をする必要がない場合は、この手法の採用の検討に値する。
サンプルコードで MazeGame::CreateMaze
を Abstract Factory パターンで実装している。
<
MazeFactory
クラスは、単に factory method を集めたものになっているが、これは Abstract Factory パターンを実装するときにもっとも一般的な方法である> (p. 102)AbstractFactory
がConcreteFactory
を兼ねるのも一般的な実装方法 (p. 102)
Builder¶
同じ作成過程で異なる表現形式の複合オブジェクトを生成できる (p. 105)
<変換すべきフォーマットのすべてを事前に確定できるとは限らないので、読み取り部を修正することなく、新たに与えられたフォーマットへの変換が容易に行えるようにしておくことが望ましい> (p. 105)
<
RTFReader
オブジェクトが RTF の文書の構文解析を行い、その結果をTextConverter
オブジェクトを使って変換する> (p. 105)「何に」変換するかはまだ言及していないことに注意。
この各変換クラスを builder と呼び、読み取り部のクラスを director と呼ぶ (p. 106)
<
RTFReader
クラスの構文解析アルゴリズムは再利用することができる> (p. 106)オブジェクトの作成プロセスが、多様な表現を認めるようにしておける (p. 106)
当パターンのクラス構造を見ると、
Director
がBuilder
を持っている。面白いのはDirector
がBuilder::BuildPart
メソッドしか利用していないこと。ConcreteBuilder
は <Product
オブジェクトを取り出すためのインターフェイスを提供する> (p. 107)Product
クラスは <多くの構成要素からなる複合オブジェクト> (p. 107) である。
このパターンはトレード・オフがないのか。
<別の
Director
オブジェクトが同じ構成要素からなるProduct
オブジェクトを作成する場合に、それを再利用することができるようになる> (p. 108)各
ConcreteBuilder
がそのまま(変更せずに)再利用できると強調している。<生成要求の結果を、それまでに得られている
Product
オブジェクトに単純に追加していくだけのモデルで十分な場合が多い> (p. 109)<異なる
Product
オブジェクトに共通の親クラスを作るメリットは少ない> (p. 109)なるほど。
サンプルコードでは MazeGame
の例を Builder パターンを導入して書き直している。
MazeBuilder
クラスにメソッド群BuildXXXX
を定義するメリットは、各Product
(Room
,Door
) の生成ロジックを隠蔽することにある。 <異なる種類の迷路を作成する場合にMazeBuilder
クラスを再利用できることを意味している> (p. 110)<
MazeBuilder
クラスは迷路そのものを作るのではなく、迷路作成のためのインターフェイスを定義しているにすぎない> (p. 110)<しかし、
Maze
クラスを小さくしておくことで理解や修正が容易になるという利点があり、また、StandardMazeBuilder
クラスはMaze
クラスから容易に分離することもできる。もっと重要なことは、この 2 つを分離しておくことにより、部屋、壁、ドアに対して、異なるクラスを使ってさまざまなMazeBuilder
クラスを作れるようになるという点である> (p. 112)Abstract Factory パターンは <複合オブジェクトを作成するという点で Builder パターンに類似している> (p. 114)
Factory Method¶
フレームワーク寄りのデザインパターンらしい。
フレームワークにしばしば見られる特徴 (p. 115):
オブジェクト間の関係を表現するのに、抽象クラスを用いる。
(具象型のわからない)オブジェクトの生成を行う責任がある。
フレームワークの立場としては、<
Application
クラスはDocument
のどのサブクラスがインスタンス化されるのかを事前に知ることはできない> (p. 115) ので、Application``クラスに ``Document
を生成するオーバーライド可能なメソッドCreateDocument
を用意し、ユーザーにサブクラス型を返すような実装をさせる。この
CreateDocument
みたいなものを一般に factory method と呼ぶ。<クラス内部でオブジェクトを生成する場合、直接生成するよりも factory method を使うほうが柔軟性を高める> (p. 117)
図形操作ツールの話 (pp. 118-119) が面白かったので、後でもう一回読んでみる。
Figure
インタフェースにCreateManipulator
(factory method) を与えておき、各Figure
のサブクラスがそれに応じたManipulator
のサブクラスを生成する、というトリック。Creator
クラスを抽象クラスにして、factory method を空にする場合と、Creator
クラスを具象クラスにして、factory method にデフォルト実装を与える場合がある(p. 118)Factory Method パターンの変形として、<factory method が数種類の
ConcreteProduct
オブジェクトを生成できるようにしておく> (p. 119) ものがある。種類を表すパラメータを取るようなメソッドにするらしい。この手法はシリアライズ実装で使うというようなことが書かれている。
言語によっては <インスタンス化されるクラスを返すメソッド> (p. 120) を使う。オブジェクトではなく、クラス自体を返すということ。
C++ では
Creator
クラスのコンストラクタ内で factory method を呼び出せない (p. 121)そんなことをしたら実行時エラーが起こって即終了。
C++ ではさらに <テンプレートを用いてサブクラス化を避ける> (p. 121) 技法も駆使したい。
factory method には、見てそれとわかる名前を付けると便利 (p. 122)
サンプルコードでは MazeGame::CreateMaze
を factory method で実装している。
<factory method は、ツールキットやフレームワークの中で広く採用されている> (p. 124)
Abstract Factory パターンは factory method を使って実装されることが多い (p. 125)
Prototype¶
既存インスタンスをコピーすることで新たなオブジェクトの生成を行うパターン。
「動機」に書いてあること
Graphics
: 音符、休止符、譜表、等々の図形オブジェクトを表現するための抽象クラスTool
: ツールパレット上のツールを定義するための抽象クラスGraphicTool
:Graphics
をドキュメントに追加するためのTool
のサブクラス<
GraphicTool
クラスは、音符などのクラスのインスタンスを楽譜に加えるためにどのように生成したらよいのかを知らない> (p. 127)<
Graphic
のサブクラスのインスタンスをクローン化して、新たなオブジェクトを生成する方法> (p. 127) によって得られるインスタンスのことを prototype と呼ぶ。
<Prototype パターンは、
Client
オブジェクトに対してインスタンス化する具象クラスを隠蔽している> (p. 129)<たとえば回路設計エディタでは、回路をいくつかの部分回路から作成するようになっている> (p. 130)
つまり、部分回路が繰り返し使われる状況である可能性が高く、そうなれば当パターンの守備範囲だ。
<C++ のようにクラスが first-class オブジェクトとして扱われない言語> (p. 130) にとっては、
Creator
のクラス階層を作らずに済む当パターンにメリットがある。<prototype マネージャ> (p. 130)
<その内部に複製をサポートしていないオブジェクトや循環する参照を持つオブジェクトを含む場合> (p. 131) 等、 prototype 各サブクラスで
Clone
を実装するのが困難な場合もある。
実装ポイント
Prototype パターンは C++ のような静的な言語において有効なパターンである (p. 131)
<
Client
オブジェクトは prototype を直接扱うのではなく、登録されている prototype オブジェクトを検索したり、新たに登録したりする> (p. 131)prototype マネージャは連想配列ベースのデータ構造。
<もっとも困難な点は、
Clone
オペレーションを正しく実装することである> (p. 131)<複製を行うということを、元のインスタンス変数を共有させることにするのか、またはインスタンス変数の複製を行うことにするのか> (p. 131)
お手軽な
Clone
の実装例として、もしオブジェクトがSave
/Load
オペレーションを提供しているのであれば、これで実装できると言っている (p. 132)
サンプルコードのページでは MazeFactory
の Prototype パターン版を紹介。
MazePrototypeFactory
では <生成オブジェクトをあらかじめ prototype として持つように初期化> (p. 132) する。MazePrototypeFactory::MakeXXXX
ではXXXX
型メンバーデータの prototype に対してClone
を呼び出し、戻り値をそのまま返す。場合によっては
Clone
のパラメータを修正する。
<他の迷路を作成する場合には、
MazePrototypeFactory
オブジェクトを別の prototype で初期化すればよい> (p. 133)
<
Client
オブジェクトの側では、Clone
オペレーションの返却値を望む型にダウンキャストしなくてもよいようにしておくべきである> (p. 135)
Singleton¶
ここは読まなくていいや。
まとめ¶
オブジェクトを生成するクラスをサブクラス化する方法
Factory Method パターンを使うことに対応。
生成するオブジェクトのクラスを把握しているオブジェクトを定義してから、それをパラメータにする方法
Abstract Factory, Builder, Prototype パターンの基本。設計は柔軟だが、より複雑 (p. 146)
図形エディタフレームワークを設計するのならば、Factory Method パターンがもっとも使いやすいパターン (p. 145) だが、
GraphicTool
のサブクラスが多く必要になる。<全般的に見て、Prototype パターンが図形エディタフレームワークにとって、おそらく最適なパターンになるだろう> (p. 146)
Graphic::Clone
のオーバーライドだけでよいから。<Factory Method パターンを使うことで、設計はカスタマイズが容易になると同時に若干複雑になる> (p. 146)
設計の初期段階では Factory Method パターンを採用しておき、様子を見て他のパターンに発展させていくやり方がよい (p. 146)
どの方法も複雑であると言っている?
第 4 章 構造に関するパターン¶
<クラスやオブジェクトを合成する方法に関係している> (p. 147) なるほど。構造イコール合成なのか。
構造に関するパターンも、「オブジェクトに適用するもの」と「クラスに適用するもの」がある。前者が動的で後者が静的な性質のものだということなのだろう。
Adapter¶
このセクションは他のパターンのそれに比べて妙に長く感じた。
<再利用を目的として設計されたツールキットクラスは、そのインタフェースがアプリケーションの要求するドメインに特化したインタフェースと一致しないというだけの理由で、再利用できないことがある> (p. 149) もったいない話だ。
既存のツールキットクラス TextView
をうまく再利用して、LineShape
や
PolygonShape
のテキスト版と言える TextShape
というクラスを定義できないかを議論している。
<それに対して、テキストの表示と編集を行う
TextShape
クラスは、基本的なテキスト編集の歳にも、複雑な画面の更新やバッファの管理などをしなければならないため、実装はより困難であると考えられる> (p. 149)<しかし
TextView
クラスを変更するのは勧められない。なぜならば、このツールキットが、ある 1 つのアプリケーションを動作させるためだけに、ドメインに特化したインタフェースを採用したとすると、このツールキット自体が汎用性を欠くものになってしまうからである> (p. 149)ここでやりたいことは
TextView
をShape
に適合させること。方法 1:
Shape
のインタフェースとTextView
の実装を継承したクラスを定義する。方法 2:
TextView
を持ったクラスを定義し、それはShape
インタフェースを有する。
→クラスに適用する
Adapter
と、オブジェクトに適用するパターン (p. 152) があるということ。<
Shape
のどのオブジェクトも、ユーザがインタラクティブにドラッグして別の場所に移すことができるようになっているべきである。ところが、TextView
クラスは、それができるように設計されていない> (p. 150)
考慮すべき問題点を挙げている。
何らかのインタフェースに一致させる作業が必要になるが、<作業の範囲は、オペレーションの名前を変えるだけの簡単なインタフェースの変更から、まったく異なるオペレーションの集合をサポートすることまでが考えられる> (p. 152)
<インタフェースの適合機能が作りこまれているクラスを pluggable adapter と呼んでいる> (p. 153) の例として、
TreeDisplay
を紹介している。異なる木構造は異なるインタフェースを持つことになるだろう。
言い換えると、
TreeDisplay
ウィジェットはインタフェース適合機能を内部に組み込むべきなのである。
実装にも問題点が色々。
C++ の場合、クラスに適合するタイプの
Adapter
では、Adaptee
側クラスをprivate
継承する。ということは、Adapter
クラスはAdaptee
クラスのサブクラスではなくなる (p. 154)適合させなければならない最小限のオペレーションの集合を意識すること (p. 154)
サンプルコード。<オブジェクトを基にした adapter の方が、コードの作成では若干の労力が必要になるが、より柔軟なものになっている> (p. 159) ポイントは、TextView
のサブクラスでも OK だというところ。
関連パターン。<アプリケーションにとっては、adapter よりも decorator の方が透過性が高い> (p. 161)
Bridge¶
最初に書いてある <抽出したクラスと実装を分離> の意味がわからない。
別名が Handle/Body とある。
動機ではクロスプラットフォームなウィンドウクラスライブラリの話を例に出している。
<さらに悪いことには、すべての種類のウィンドウに対して、2 つずつ新たなクラスを定義していかなければならなくなるだろう> (p. 163)
<この
Window
クラスとWindowImp
クラスの間の関係を bridge と呼ぶ> (p. 164)
適用可能性を見ると、クロスプラットフォーム以外にも使い途がある。特に C++ で威力を発揮するケースがあるようだ。
<クライアントのコードを再コンパイルしなくても済む> (p. 165)
<クラスの実装をクライアントから完全に隠蔽したい場合。C++ では、クラスの内部表現はクラスのインタフェイスで見ることができてしまう> (p. 165)
Exceptional C++ とかで議論していた Pimpl パターンの話を思い出す。
クラス構造を見ると一発で理解できる。
Implementor
クラスが一種類しかない場合でも、クラスの実装上の変化がクライアントに影響を与えることがあってはならない場合には、Abstraction
/Implementor
分離は有効 (p. 167)C++ の場合、
Implementor
の宣言を <私的なヘッダファイル> (p. 167) で行う。要するにクライアントが include できないファイルで宣言する。Implementor
の決定を他のオブジェクトに完全に委譲するという方法もある (p. 167)例えば
Implementor
の決定を専用の factory が行うことにすると、Abstraction
クラスとImplementor
クラスの結合も間接的になる。
サンプルコードを検討すると、次のことに気付く。
Window
のサブクラスのメソッドの実装は、すべてWindowImp
のメソッドで実装している。// p. 170 void Window::DrawRect(const Point& p1, const Point& p2){ WindowImp* imp = GetWindowImp(); imp->DeviceRect(p1.X(), p1.Y(), p2.X(), p2.Y()); }
WindowImp
のサブクラスでのメソッド実装は、そのプラットフォームの API で実装している。例えばXWindowImp::DeviceRect
は関数XDrawRectanele
で矩形を描画する、といった具合だ。Window::GetWindowImp
は Abstract Factory パターンでインスタンスを取得している。
Composite¶
<オブジェクトを木構造に組み立てる> (p. 175)
<個々のオブジェクトとオブジェクトを合成したものを一様に扱うことができる> (p. 175)
<Composite パターンの特徴は、1 つの抽象クラスがプリミティブとコンテナの両方を表すことである> (p. 175)
プリミティブの意味がよくわからんが、<プリミティブなオブジェクトは子を持たないため、子オブジェクトに関するオペレーションは実装しない> (p. 176)
<
Draw
オペレーションをその子オブジェクトのDraw
オペレーションを呼び出すように実装し、またそれ以外にも、子オブジェクトに関連するオペレーションを実装する> (p. 176)<
Picture
オブジェクトは別のPicture
オブジェクトを再帰的に生成していくことができる> (p. 176)Component
クラスにおいて、親にあたるcomposite
にアクセスするインタフェースを宣言するのはオプション (p. 177)ある要求を
composite
が受け取ったとき、<通常、その要求を子にあたるcomponent
に転送し、さらに転送の前後に付加的なオペレーションを実行することもある> (p. 177)
実装のセクションにある記述が濃い。
親オブジェクトへの参照を持たせる場合、
composite
構造の走査や管理が簡単になるが、「あるcomposite
のすべての子オブジェクトは、その親オブジェクトとしてそれを持つ」という制約を壊さないように注意しないといけない (p. 178)<
Component
クラスでサポートされているが、Leaf
クラスには無意味なオペレーションも多く存在する> (p. 179)Add
/Remove
オペレーションをどのクラスで宣言するかは重要な問題。この議論に 2 ページ近くを割いている。普通は安全性を捨てて、透過性をとる方向に解決するのだろう。多くの設計では、<子オブジェクトの順番を明確にする> (p. 182)
<
composite
に、自身が削除されるときにその子オブジェクトの削除も一緒に行わせるようにするのが、通常ではもっとも良い> (p. 182) が、子オブジェクトが共有されているような場合は話は別だ。
Decorator¶
<サブクラス化よりも柔軟な機能拡張方法> (p. 187) を動的に行えるようだ。
<クラス全体に対してではなく、個々のオブジェクトに責任を追加したくなることがある> (p. 187)
いつぞやのスクロールバー付き枠付き
TextView
の例を持ちだしている。<常にスクロールバーが必要とは限らない>
<必要になったときには
ScrollDecorator
オブジェクトを用いてスクロールバーを追加する> (p. 188)
Decorator
クラスの構造は、
<
component
またはdecorator
への参照を保持する><
Component
クラスのインタフェースと一致したインタフェースを定義する> (p. 189)
の二点。
<Decorator パターンを用いると、
decorator
を付けたりはずしたりして、実行時に簡単に責任の追加や削除ができる> (p. 190)個人的には削除の例は見たことがない。
<1 つの単純なクラスを定義し、
decorator
を用いて機能を段階的に追加していく> (p. 190)Component
クラスを軽く保つことが重要。メンバーデータは極力サブクラスに持たせる (p. 191)<
Component
クラスが本質的に重く、そのためDecorator
パターンを適用するにはコストがかかりすぎるような状況ではStrategy
パターンを選択する方がよい > (p. 191)
サンプルコード。コンストラクタの呼び出し方にインパクトあり。
// p. 194
window->SetContents(
new BorderDecorator(
new ScrollDecorator(textView, 1)
)
);
使用例。
<ストリームはほとんどの I/O 機構に存在する基本的な抽象概念である> (p. 195)
decorator は adapter とは異なる。責任を変えるだけで、インタフェースまでは変えない (p. 196)
オブジェクトを変化させる方法には、decorator と strategy の 2 通りが考えられる (p. 196)
Facade¶
後回し。
Flyweight¶
このパターンは細かいオブジェクトの共有を目的とする。
<flyweight とは、複数の文脈で同時に利用され得る共有オブジェクトのことである> (p. 207)
文脈とは何か。
共有オブジェクトということは、状態の持ち方に特別な何かがありそうだ。
<ここでキーとなる概念は intrinsic 状態と extrinsic 状態の区別である> (p. 208)
intrinsic 状態は flyweight オブジェクトの内部に格納。
文脈とは依存しない、独立した情報。
共有できる情報。
extrinsic 状態は
文脈に依存する情報。
共有できない情報。
文書エディタの例で言うと、
各文字が flyweight オブジェクトであり、
文字コードは intrinsic 状態。
座標位置、フォントは extrinsic 状態。
<extrinsic 状態に依存する可能性のあるオペレーションは、extrinsic 状態をパラメータとして渡される> (pp. 208-209)
適用可能性は、それを見極めるのがわかりやすいようだ。
非常に多くのオブジェクトを利用する。
そのためにメモリ消費コストが高くつく。
<オブジェクトの状態を構成するほとんどの情報を extrinsic にできる> (p. 209)
あとはオブジェクトを共有できるかどうか。
<
flyweight
が機能するために必要な状態は、intrinsic 状態か extrinsic 状態のどちらかに分類されなければならない> (p. 211)flyweight
オブジェクトは <FlyweightFactory
オブジェクトから入手しなければならない> (p. 211)便宜上 Factory という単語を使っているだけであって、アクセスの度に常にオブジェクトを生成しているわけではない。
<格納コストをもっとも節約できるのは、オブジェクトが intrinsic 状態とextrinsic 状態の両方についてかなりの量の情報を持ち、しかも extrinsic 状態が格納されるのではなくて計算できる場合> (p. 212)
<オブジェクトは共有されるので、クライアントがそれらを直接インスタンス化すべきではない> (p. 212)
<オブジェクトの共有では、
flyweight
が不要になったときに(略)何らかの形の参照数管理やガーベッジコレクションが必要になる> (p. 213)
サンプルコードの見どころは GlyphContext
クラス。一見しただけでは何を管理しているのか理解できない。
使用例
<
flyweight
の概念は、InterViews 3.0 における設計テクニックとして初めて記述され、研究された。その開発者は、この概念を立証するために、Doc と呼ばれる強力な文書エディタを構築した> (p. 218)180000 文字を含む文書を 480 個の文字オブジェクトで賄えるケースがあったとか。
Proxy¶
<オブジェクトの代理、または入れ物> (p. 221)
<そのオブジェクトを実際に利用する必要が生じるまで、そのオブジェクトの生成と初期化にコストをかけるのを延期する> (p. 221)
<生成に高いコストのかかるオブジェクトをすべて同時に生成するのは避けることにする> (p. 221)
<文書の中には画像の代わりに何を置いておけばよいのだろうか> (p. 221)
<要求があり次第画像が生成されるという事実を隠蔽するにはどうしたらよいのだろうか> (p. 221)
→画像 proxy なるものを導入することで解決する。
適用可能性として、4 種類の proxy を分類している。
remote proxy
virtual proxy
protection proxy: <実オブジェクトへのアクセスを制御する> (p. 223)
オブジェクトごとに異なるアクセス権が必要な場合に有用らしい。
smart reference: <通常のポインタに代わるもの> (p. 223)
結果の説明でコピーオンライトについて言及がある。
<もしコピーされたオブジェクトが変更されないのであれば、このコストを発生させる必要はない。コピーするプロセスを延期するために proxy を使えば、そのオブジェクトが変更されたときにのみ、そのオブジェクトをコピーすればよいようにできる> (p. 225)
実装
<メンバアクセスオペレータをオーバーロードする方法は、どのような種類の proxy に対しても良い解決法になるとは限らない> (p. 226)
<
Proxy
クラスがRealSubject
クラスをインスタンス化する場合(略)には、Proxy
クラスはその具象クラスを知っていなければならない> (p. 227)
まとめ¶
Composite, Decorator, Proxy の比較 (p. 234) が面白かった。
Decorator は退化した Composite ではない。
両者は目的が異なっている。
ということは、相補的に利用できる。
Decorator も Proxy も、クライアントに合成前のインタフェースと同じものを与えるが、Proxy に関しては
特性を動的に加えたりはずしたりしない。
再帰的な合成のために設計されていない。