GoFの23のデザインパターンを,Javaで活用するための一覧表 (パターンごとの要約コメント付き)
GoFデザインパターンの一覧表と,活用のためのコメント,および入門者が独学するためのリンク集(サンプルコード付き)。
入門者の独学を支援するために,このページのURLを提示して熟読させ,各パターンを短時間で効率よく学んでもらう。
デザインパターンはプログラマの常識だ。 Java使いかどうかは問わない。
にも関わらず,入門書を買ったまま,途中で挫折する人が多い。
挫折の原因は,パターンの数が23もあって,多いからだろう。
全パターンをすんなり覚えてもらうためには,各パターンごとに
- 「要するにこういう目的のパターンなんだ。」
- 「10文字で表現すると,パターンの意味はこうなんだ。」
という要点・本質を,短いコメントで伝えれば助けになるだろう。
こういった学習を通して,Java言語の「設計思想」も併せて感じ取ってゆけるはず。
全パターンの一覧表(要約コメント付き)
全パターンについて,10文字以内での要約コメントを付記した一覧表。
範囲 | |||
---|---|---|---|
クラス | オブジェクト | ||
目 的 |
生 成 |
・Factory Method: 動的にサブクラス選択 |
・Abstract Factory: 工場の工場 ・Builder: 初期化手順を細分化 ・Prototype: コピーを渡す ・Singleton: 1インスタンスを保証 |
構 造 |
・クラス向けAdapter: 継承でラッパー |
・オブジェクト向けAdapter: 委譲でラッパー ・Bridge: 拡張と実装の階層分離 ・Composite: 再帰ツリー構造 ・Decorator: 委譲で意図的フック ・Facade: 複数クラス利用手順書 ・Flyweight: キャッシュ付の工場 ・Proxy: こっそりフック |
|
振 る 舞 い |
・Interpreter: 独自言語の実行 ・Template Method: 子が処理断片を具体化 |
・Chain of Responsibility: 助け船ネットワーク ・Command: タスクキューとスタック ・Iterator: 並んだ物を順番に処理 ・Mediator: スター状の相互作用 ・Memento: 状態のゲッタとセッタ ・Observer: イベントリスナ ・State: 状態オブジェクト ・Strategy: アルゴリズム切り替え ・Visitor: 構造の便利スキャナ |
※この表からコメントを除去したバージョンが,本エントリ末尾にある。知識のテストに利用できる。
これらのパターンについて,「要するにどういうパターンか?」というのを,順を追ってわかりやすく解説する。
レベル別に分類して紹介し,23個全部をカバーする。
各パターンのシンプルな分類と説明
パターンの説明を,慣習に倣って「Abstract Factory」から始める*1のは,無理がある。
もっとわかりやすい順番が色々ある。ここでは,入門しやすい順に掲載した。
エントリの後半に,独学用のリンク集がある。それを見ながら,
各パターンの「本質」はこういう事ね,と納得しながら読み進めるとよい。
(分類1)プログラミングのセンスがあれば,言われなくても自然に使うもの:
パターンとしていちいち取り上げる必要性も薄いほどの物。
逆にそれだけ,オブジェクト指向のプログラミング言語の入門者にとっては「初級の段階で必須の知識」という事になる。
- Template Method: 子が処理断片を具体化
- Builder : 初期化手順を細分化
- あれこれ注文を付けてから,最後にドカンと一発やらかすパターンである。最終的な処理をカスタマイズするために,事前に設定や命令を1個ずつ言い渡す事ができるパターン。とも言える。1個ずつ言い渡せるので,おかげでコンストラクタ内の初期化処理に何もかもを詰め込まないで済む。
- 各種言語の各種ライブラリは,ユーザに対して選択の自由を与えるために,この形式を頻繁に取っている。いろいろ属性を設定させて,最後に画面表示とか,よくある。
- とはいえ,このパターンではBuilderが特定のインタフェースを実装する事になっており,Builder自体の交換可能性が増している,という点もポイント。
- Adapter : 継承でラッパー/委譲でラッパー
- 単なるラッパークラスである。これだけだとパターンと言うまでもないのだが,「インタフェース(システム側からの見え方)の差し替え」という視点で見てみると,わざわざ言及される理由が少しは納得できるかもしれない。
- ベンダが提供する既存APIの出来が悪く,これから作るシステムとの相性も悪い(=共通のインタフェースを実装していない)。なのでそのまま使いたくないのだが,APIの改造は禁止されている,とする。その場合,自分が好きなやり方でそれらのAPIを利用できるように,一段かませて「便利クラス」を作る。この便利クラスは,自分たちのシステムで使いやすい形式になっている(=共通のインタフェースを実装している)。
- なおラッパークラスを作るには,元のクラスを継承してクラス単位でwrapするか,元のクラスのインスタンスをコンポジションで保持してオブジェクト単位でwrapするか,の2パターンがある。なので,Adapterパターンは前掲のマトリックス中の2か所に現れる。
- ※付加的なテクニックだが・・・,Adapterパターンをうまく使えば,開発チーム内の対人関係が良くなる,という効用もある。低スキル者が滅茶苦茶なコードを書いた際,そのクラスのソースコード自体に横から手を出して他のメンバが書き変えようとすると,最初にコーディングした方のプログラマは「傷ついた」と感じるものだ。それを避けるために,ラッパークラスを一段かませて,ラッパークラス側を充実させて高品質にする。もとのクラスには手を触れないで,原作者の学習課題として保留にしておく。こうすれば,レベルにギャップがあっても低品質なコードの影響を最小限にとどめ,開発工程の品質保持と平和なチームの維持が可能になる。Adapterは,低品質また粗悪なコードからシステムを守るための「防波堤」のような役割をするのだ。
- Strategy: アルゴリズム切り替え
- Decorator : 委譲で意図的フック
- 操作対象にしたいオブジェクトをコンポジションで保持し,そいつにあれこれ操作を加える(Decorateする)。
- コンポジションを使っていればいつの間にかそうなっているはず。これぞDecorator,と意識しながら使う程のものでない。Proxyパターンと酷似。
- コードの見かけ上,DecoratorはDecorateeを引数に取るなどして外側をくるむ。つまり,内側のDecorateeが行なうメイン処理を,外側からDecoratorが何かフックしているのかな,という推察が可能。(しかしやはり,そこで特にDecoratorという名前が持ちあがるわけではない。)
- そして保持する側と保持される側が同一のインタフェースを持っているので,Decorateされてる物とされてない物を同一視できる。フックのかかった物と,フックされなかった物とを共通項として取り扱えるわけだ。
- Facade : 複数クラス利用手順書
- 複数のクラスを呼び出す際,呼び出す順番を思い出さなくてすむように,定石として1メソッド内に集約しておく。そのメソッドは,いわば複数のクラスの利用手順書になる。
- 再利用しやすいAPIやDSLを作ろうとする場合,自然に生まれる発想。低レイヤのコードをいじらなくて済むよう,ひとまとまりの手順としてまとめておくのである。こうすれば,より高レイヤのコードに神経を集中できる(Write less, Do more)。逆にもし自然にこうならないとしたら,プログラミングのセンスが微妙かもしれない。
- なおfacadeパターンには,「いつも同じ窓口を通過させることにより,共通処理を全体に埋め込みやすくする。」という使い方もある。複数クラスへの分岐地点としてfacadeクラスを設置し,その分岐地点の中にログ出力などの共通処理を埋め込めば,他の個別のクラスには手を加えなくて済む。
- Mediator : スター状の相互作用
- 網目状の複雑な相互作用ではなく,中央にコントロールを集約した「スター状の結合」を介して,オブジェクト同士が相互作用する。中央にいるのは,管理者オブジェクトのようなもの。
- あるUIが複雑で,たくさんの部品を持っていて,各部品ごとに制約やイベントがたくさん存在し,それらの条件分岐も大変・・・というような時に,素人はあっちこっちにイベントを埋め込む。そして全体として何をやっているコードなのか分からなくなり,そのうちメンテできなくなり,整合性が損なわれるようになる。こういった滅茶苦茶に分散したコードに付き合わされるのがどれほど大変な事か,経験者はその苦労を語る事ができる。
- プログラミングのセンスがある人間は,そういう実装はしない。このUIはある程度複雑だな,と感づいたら,そのUI専用のマネージャクラスを立てて,各UI部品はそのマネージャクラスに対して通信するだけにする。そして,マネージャクラスは自分のもとに集約された情報をもとにして,UI全体を調整する役割を果たす。誰にも言われなくても,こういうプログラミング・スタイルを選択しようと自然に思い立つかどうかで,プログラミングのセンスが問われる事になる。
- そういうわけで,Mediatorパターンは「複雑なUI」を実装する際によく使う。ただし,このパターンは「中央集権的」な発想であり,余りにも複雑すぎるUIになってくると中央で管理しきれなくなって,限界がくる。そういう場合は,後述するChain of Responsibilityパターンで,中央集約ではなく分散構造/バブリングの発想に切り替える。
(分類2)パターンの名前自体が有名で,あちこちで使われているもの:
各種ツール・ライブラリを使った経験があったり,業務経験を少し積んだり,そこそこプログラミングをやっていれば,いずれ目にする名前のパターン。
見た事がないとしたら,経験自体が少ないという事になるかもしれない。
- Factory Method: 動的にサブクラス選択
- サブクラスの中から選択する挙動を,動的に変えたい場合に。
- Javaの各種フレームワーク上でDB操作していれば,いずれ目にする名前。Connection系のFactoryとか。
- クラス単位でFactoryではないとしても,メソッド単位でFactoryになっているケースも多い。newキーワードを隠ぺいして何かのインスタンスを動的に生成して返していたら,そのメソッドはファクトリ・メソッドと呼ばれうる。
- クラス設計の戦略がFactoryである場合,もしも何かのはずみで同一箇所にGenericsも導入しようとすると,たいへん悲惨な結果になる。Factoryは性質の異なる子クラスを増やす方針なわけだが,ジェネリクスはその真逆であり,たくさんあるクラスを型パラメータで1つに統一しようとする。なので,この2つの方針がぶつかりあうと,どこかで一斉にコードの矛盾が生じる。その矛盾に気づくのは,Factoryクラスに型パラメータが入り込もうとする瞬間である。「あれ?クラスを分けようとしてたんだっけ,それとも統一しようとしてたんだっけ?」という疑問で手が止まり,しばらく考えた後,Factoryとジェネリクスを両立させようとしていた自分の愚かさに気づくのだ。そんな目に遭わないためには,Factory等を始めとする「ポリモーフィズムをフルに生かして透過的に扱う」という設計パターンと,「型パラメータで抽象化して透過的に扱う」という設計戦略とが,ほぼ対極に位置するため相性が悪い。という点を覚えておいた方がいい。
- Singleton : 1インスタンスを保証
- プログラミング言語によっては,これを実現するためのライブラリが言語に組み込み済みなケースも多い。なので知名度は高い。
- シングルトンには落とし穴が結構あるので,よくわかっている人なら,このパターンの利用を避けるケースが多いと思う。例えばグローバル変数問題は言わずもがな。さらに例を挙げると,Webアプリは複数のリクエストを同時に複数スレッドで受け付けるわけだが,スレッドという枠組みを超えて,複数プロセスでWebサーバのプロセスが走っているというシチュエーションも一般的である(Apache/httpdとか)。その場合,プロセス間ではメモリ空間が異なるためオブジェクトは共有されないので,頑張ってシングルトンを作ったのに本番環境では全く無意味でしたなんていうポカも珍しくない。これは,洒落にならないレベルのダメージを生む設計ミスである。こういう時,周りのメンバが「あっ,こいつSingleton使おうとしてるのかも…?」と感づき,必死で止めに入り,基本設計から再考するように説得する。しかし説得の際に,メモリ空間とかサーバプロセスに関する知識が相手に欠けていると,なぜダメなのか説明に苦労する羽目になる。という具合に,半可通が手を出すと大火傷するパターン。
- Observer: イベントリスナ
- Observerという名前ではなく,Listenerという形で,知らないうちに使いこんでいるはず。
- 「イベントリスナ」という形で,動的なUIを書いた事がある人には馴染みのある概念のはず。監視対象にイベント観察者を埋め込んで,イベントが起こる時に何かさせる。JavaScript使いなんかには即通じる。
- ただしJava言語にはクロージャがないので,イベントハンドラは○○Listenerクラスみたいな形で記述する必要が生じる。そこが冗長だが,Java言語というプラットフォームを使う以上はしょうがない。妥協点である。
- Proxy: こっそりフック
- プロキシ・サーバ(またはリバースプロキシ)という物の存在のために,名前だけは知られているパターン。中継するイメージが共通。
- メインな処理をするオブジェクトをコンポジションで保持し,メインな処理の前後にフックをかませる。しかも,保持対象と保持者で共通のインタフェースを持っているために,相互の入れ替えがきくという点は,Decoratorパターンと全く同じ。
- Decoratorは意図的明示的にフックしていたが,Proxyの場合は影で,隠れて,こそこそとフックしている点が異なる。クラスを使う側は,Decoratorパターンの場合は,呼び出し側は意図的にDecorate処理のコードを書く。しかしProxyの場合は,呼び出し側は「保持対象」と「保持者」のどちらを使っているのか意識しない。もしくは,気づけない。
- 処理にこそこそとフックする,というのの具体例は,例えば「ネットにつながっていると思ってたら,実は違ってて,間に挟まっているプロキシサーバがキャッシュを投げてるだけだった」とか。メイン処理をするオブジェクトと呼び出し元の間に,こっそりと壁を作る(仲介者もしくは盗聴者を置く)事ができるのだ。
- 別のオブジェクトへの中継地点となって本来の処理を隠ぺいしているメソッドは,プロキシ・メソッドなどと呼ばれる。呼び出し先は別のオブジェクトなので,これは単なるカプセル化とは異なる。
- Iterator: 並んだ物を順番に処理
- Java言語にあらかじめ組み込まれていない事が不思議で,これはJava言語の仕様のバグなんじゃないか?と思わせるほど,あって当たり前のプログラミング・テクニック。言語によっては標準APIとして提供される。
- 「複数のものが順番に並んでいて,取り出せるようになっている」という,集合体(または配列)みたいなオブジェクトがあるとする。ものを順番に取り出して処理するのは,そのオブジェクト自身の責任である。そのオブジェクトを呼び出す外部オブジェクトの責任ではないはずだ。よって,「自分(配列っぽいオブジェクト)が抱えている要素達の面倒を,自分自身が見てあげる」べき。それがIterator。
- したがって,Javaは純粋なオブジェクト指向の言語ではない。オブジェクト指向ではなく,アセンブラやポインタといった低レイヤの考え方を好むC言語ユーザをJavaユーザに呼び込んで,Javaのシェアを拡大するためには,Sunはそうするしかなかったのだ。forループ内でカウンタ変数をインクリメントしながら一つ一つ取り出す,という,C言語ユーザにとってなじみ深い書式を採用するしかなかったのである。拡張forループと言えど,クラス構造の観点で言えば同じこと。つまり,マーケティング上の事情により,Javaはオブジェクト指向の言語になる事ができなかった。この歴史的な経緯は,下記の書籍などでも指摘されており,Javaをちゃんと使っている人なら皆が既によく知っている点だ。
オライリー,「プロダクティブ・プログラマ」,Neal Ford著。
- セクション14.1.2「Javaの暗黒面」p212〜213。ポインタが多用されるC言語との後方互換性に配慮したおかげで,CやC++からJavaへの移行が楽になった。(Javaはオブジェクト指向でポインタがないにも関らず)
Javaの上級者なら,Javaの長所だけでなく,欠点についてもよく知っているはずなので,彼らに尋ねてみるといい。
※逆に万が一,こういったJavaの欠点をすらすら言えないとしたら,そのプログラマはJavaを十分使っていない(もしくは,Javaしか知らないのでJavaで閉じた発想法しか持っていない)ということになる。
これはJavaに限らず,何かを習得する際には,全ての事に当てはまる。
-
- ちなみにRubyのような純粋なオブジェクト指向の言語であれば,モデル層の記述はイテレータ(each系のメソッド)尽くしのメソッドチェインになったりする。findしてmapしてselectしてmapしてrejectしてuniqしてconcatしてjoin(またはinject)とか,極めて生産的かつ「思った通りに書けば思った通りに動く」を実感する。最近のプログラミング言語では,列挙されたデータの処理を記述する際にはそういうスタイルがだいたい可能になっている。Javaではこれと同じ事ができないので,記述が非常に冗長になる。なぜJavaで実現不可能なのか,を突き詰めると,結局「クロージャ(ラムダまたはブロック)を定義できないから」また「オープンクラスではないから」という言語仕様の難点に尽きる。JDK8から先の動向をウォッチすべし。
- もし今後,JDKがだめそうなら,JVMプラットフォーム上で動く生産的な動的言語(GroovyとかJRuby等)をウォッチすべし。世間でよく言われるように,Javaは「現代のCOBOL」であって,COBOLと同様にある時期には非常に有用かつメジャーな商業的ツールではあるものの,何事もタイム・リミット付きなので,「先々をよく見越した上でスキル・ポートフォリオを考えないと,エンジニアとして化石化してしまいますよ。」という警告に留意しながら技術動向にキャッチアップしてゆけば安全という事になる。手続き型言語からオブジェクト指向へのパラダイム・シフトが起こった時のように,現時点でも既にビジネス要求の変化が加速しており(要するに時代の変化のスピードがどんどん速くなり),静的言語がその「時代のスピード」の要求にいつまで耐えて持ちこたえられるか,見ものである。それまでの間はJavaを使いましょう。
(分類3)独力で思いつくのは困難だが,一度ちゃんと学べば,驚くほどプログラミングが開眼するもの
このレベルの習得が分かれ目。
自力で思いつく範囲内で設計を続けていると,ここには到達できない。
これらのパターンでは,凡人の発想では思いもよらなかったような対象物/構造をオブジェクト化する。
なので,オブジェクト指向設計の高みを目指す場合,このステップはきわめて有力な踏み台になり得るのだ。
ちょうど「ORマッピング(ObjectとRDB上のリレーションをマッピング)」のように,「Object-○○マッピング」という視点をすると,理解が早い。
「このパターンの場合,○○の部分には何が入るんだろう?何がオブジェクト化されているんだろう?」と。
- State: 状態オブジェクト
- システム開発の全工程を通じて,「状態遷移図」は非常に重要な役割を果たす。これは詳細設計フェーズの成果物であり,仕様変更に伴い変更されやすい。単体試験〜結合試験まで,テストケースの洗い出しのためにも極めて重宝する。システム運用フェーズでは,障害発生時にシステムサポート側がしょっちゅう見直す図となる。システム中でこの部分は,考慮漏れバグとか不整合が発生しやすい箇所だからだ。
- Stateパターンは,個々の状態を表すための専用オブジェクトを作る。なので,状態遷移図ベースのダイレクトなコーディングが可能になる。いわば,Object-状態遷移図マッピングである。そういう経緯もあり,Strategyなんかと比べるとはるかに,システム設計の観点からして望ましいパターン。
- Command: タスクキューとスタック
- Stateでは1状態=1オブジェクトだったが,Commandでは1タスク=1オブジェクトとなる。タスク(またはイベント)が,1つのクラス内にカプセル化される。そういうタスクが何種類もあり,共通のインタフェースを実装しているので,複数のタスクを同種のものとしてまとめて取り扱って管理できる。要は,タスクマネージャっぽいものを実現できる(※Windowsの文字どおりの「タスクマネージャ」とはイメージが異なるが)Object-タスク・マッピング。
- 複数のタスクを保管する際に,終わったタスクをスタック構造で保持すれば,「作業履歴」として参照可能になる。また,スタックの履歴内容に対してpushやpopすれば,直近タスクの「アンドゥ」や「リドゥ」も可能になる。
- 逆に,処理待ちのタスクはキュー構造で保持する。各タスク(Commandオブジェクト)が,自分自身のタスク完遂のために必要なリソースの見積もり情報を保持していれば,複数のタスクの実行過程において「残り所要時間」とかを算出する事も可能になる。つまりは,プログレスバー。
- そういうわけで,インストーラのプログレスバーとか,アンドゥ・リドゥは,こうやって実装されていたのか!と開眼するきっかけになる。文字通り,操作・命令(「command」)に対する概念が変わるはず。
- Memento: 状態のゲッタとセッタ
- オブジェクトの状態のバックアップと復元が可能になり,アンドゥみたいなことができるようになる。バックアップ対象のオブジェクトには,そのオブジェクトの状態をバックアップするための専用のMementoオブジェクトを生成可能にする(状態のgetter)。また,バックアップされた状態オブジェクトを「食わせる」事により,特定の状態に復元させるためのメソッドも用意する(状態のsetter)。こうする事で,オブジェクトのカプセル化を保ったままで,オブジェクトの状態をバックアップ・復元できる。オブジェクト-バックアップ・マッピング。
- getter/setterという概念や,オブジェクトのatomicな更新といった概念は慣れ親しんでいるだろう。でも,それを活用して特定のオブジェクトの「スナップショット」を取得&適用可能にする,という発想は,自然には生まれづらい。
- Bridge: 拡張と実装の階層分離
- このパターンだけ,メリットの説明が少しだけ長くなる。Bridgeは下記のような悩みを解決するパターンである。
- 「たった1つの新しい機能を追加するたびに,全環境用の,全機能のコードをそれぞれ1から実装し直さなければならない…。」
- Bridgeパターンを適用すれば,機能追加により生まれる拡張階層と,サポート環境追加により生まれる実装階層を,分離して別個に管理できる。その結果,下記のように状況が変化する。
- 「たった1つの新しい機能を追加するたびに,全環境用のコードの修正個所は,それぞれ1か所ずつで済む。」
- Bridgeパターンのメリットを,図を使って説明してみる。
- このパターンだけ,メリットの説明が少しだけ長くなる。Bridgeは下記のような悩みを解決するパターンである。
■Bridgeパターン適用前: ↓ Abstract ┌─環境タイプ1 (Windows用実装) ↓ 機能ver.1 ←─┼─環境タイプ2 (Mac用実装) 機↓ ↑ └─環境タイプ3 (Linux用実装) 能↓ | 仕↓ Abstract ┌─環境タイプ1 様↓ 機能ver.2 ←─┼─環境タイプ2 の↓ ↑ └─環境タイプ3 進↓ | 歩↓ ・・・ ↓ 機能バージョンアップのたびに ↓ 実装の同じ重複が延々と続く ■Bridgeパターン適用後: ↓ ↓ 機能ver.1 ◇──→ 全verの機能実現用の タ↓ ↑ API集のインタフェース テ↓ | ↑ 広↓ | ・ が↓ 機能ver.2 ・・・・・・・ り↓ ↑ ・ ・ ・ ↓ | 環 環 環 ※↓ | 境 境 境 機↓ ・・・ 1 2 3 能↓ ↓ ←─────→ ↓ ヨコ広がり ※(サポート環境別の)実装
-
- クラス図の全体像が,樹形図のような複雑さを失い,「Π」型(ギリシャ文字のパイ)になったのがわかる。この上辺が「橋渡し」を行なっている部分であり,そのためにBridge(橋)という名前がつく。
- 「機能仕様書の進歩」と,「サポート環境の種類(タイプ)の多さ」という2つの軸を見抜くことがキーになる。Bridgeパターンを使えば,これら2つの軸を「タテ広がり」と「ヨコ広がり」に分解して整理できるのだ。
- なので結論として,Bridgeパターンの要約フレーズ「拡張と実装の階層を分離する」は,「時間経過に伴って機能が拡張されていくバージョンアップの過程と,実装タイプのバリエーションを豊富に広げる品揃えの充実具合を,別個に分けて,整理して管理できる」というふうにやさしく表現できる。(それを言いたかったので,わざわざAAで図を描いた。)
- ※とはいえ,このパターンが必要になる理由は,ただ単に「Java言語はコードの共有がしづらいプラットフォームだから」とも言える。Moduleのmixin機構みたいなものが無いということ。「Javaが前提なら,こういうケースにはBridgeで対処可能だ。」と知ること自体は良いことなのだが,でもJavaしか知らないとBridgeの発想に固執してしまうかもしれない。それはそれでまずい。
- Composite: 再帰ツリー構造
- ディレクトリ構造や組織の人員構成など,再帰的な構造を実装するために必須のパターン。クラス図上では,子が親になり,親が子になる…という循環的な見かけを持つ。
- ディレクトリ構造のサンプルコードで言うと,ファイルとフォルダを同一視/抽象化して,両者に共通の性質(インタフェースまたは基底クラス)を持たせるところがポイント。この見方は,自然には生まれづらい。オブジェクト-再帰構造・マッピング。
- アルゴリズムをちょっと勉強した人なら,再帰呼び出しを使って「階乗の計算」ぐらいは実装できる。つまり処理の実行順という観点での再帰は知っている。しかしそれを知ってても,データ構造をうまく再帰的に実装し,拡張性とシンプルさを両立させる方法となると,自力で思いつくのは難しい。なので,Compositeパターンは意識的に学ぶだけの価値がある情報。
- Interpreter: 独自言語の実行
- 構文解析の結果を,オブジェクトにマッピングし,実行させるのである。ORマッピングならぬ,Object-BNFマッピングとでも言うべきパターン。クラス構造が再帰的なのはCompositeと類似。
- とにかく一回実装してみないと,効用がわからない。自分の手で,何か作ってみること。そうしないと,急にソース中に現れた時に「これはInterpreterだ」と気付く事すらもできず,調査する事も出来ず,手も足も出ない。このパターンでは個々のクラスはシンプルなので,狭い範囲で見ていると,ググって何か手掛かりを得ることは不可能なのである。もしも計算機科学のバックグラウンドが浅いためにBNFを見たことが無い場合は,事態はよりいっそう悪化する。
- このパターンの需要は,一応ある。システムのユーザに対して,何か「独特の記法」を提供するような場合に必要になる。
- Chain of Responsibillity: 助け船ネットワーク
- よく言われるのは「リクエストのたらい回し」だが,もうちょっとわかりやすくいうと「助け船のネットワーク」である。自分では処理しきれないタスクだ,と判断したら,自分が知っている助け舟にヘルプを求めて,そいつに任せてしまう。任せた先でも対処しきれなかったら,さらにその助け船が呼ばれる。このように,助け船にヘルプを求める連鎖構造を作るのがこのパターン。全体として見ると,「奥の手を何重にも持っておく」事になるのだが,それだと一つのクラスが対処方法を何重にも張り巡らして待ち構えているように聞こえるので語弊がある。あくまでも,最初の受付にあたる部分から順番にタスクが運ばれてゆき,次の行き先は現在の処理者だけが知っている,という「窓口付きのネットワーク状の分散構造」がキーになる。
- 少し専門用語を使って言いなおすと,「要求処理者の線形構造」とも言える。それぞれの処理者達は,自分がダメだった場合の次の行き先(ポインタ,参照)を持っている。だから線形構造と呼べなくもない。しかもそのポインタは,コンポジションとして実現されているので,動的な書き換えが効く。ただし,必ずしも一次元の連鎖に限定されず,前述のように網目状のネットワーク構造(循環しない有向グラフ構造)でも構わない。
- 技術的な言葉を使って説明すれば,GUIシステムにおける「イベントのバブリング」である。例えばブラウザ上のDOM構造で,JavaScriptのイベントは,親ノードへと伝搬してゆく。このエントリなどを参照。おかげで,表面から順番に奥に向かって進んでいく過程で,イベントに対処すべき適切な処理者がそのうち見つかる事になる。
- このパターンを学べば,バブリングのようなUIシステムはこうやって実現されていたのか!と気づくきっかけになる。そして,処理を行なうオブジェクト同士の協調性を高くするような発想法が生まれる。
- Visitor: 構造の便利スキャナ
- 複雑なデータ構造がある場合,データ構造を表すオブジェクトの中には具体的な処理を書かない。かわりに,自分に対して処理を行なってくれるVisitorを受け入れるというロジックだけを整えておく。そして,Visitorはデータ構造を渡り歩き,各データに対して所定の処理を行なう。Visitorはデータ構造をスキャンしながら仕事をこなしているのである。
- データ構造の側は,具体的な処理を知らなくて済む。Visitorというものが自分のもとを訪れて,何かの処理をしてくれるのだということさえ知っていればよい。また,Visitorの側も,具体的に定義されたデータ構造を知っている必要はない。データ構造の最低限のインタフェースを知っていればよい。そして,データ構造内での探索を行ない,また各データに対する具体的な処理を行なう。このようにして,データ構造と処理が分割される。
- 要は,visitorはデータ構造に対する「処理付きの便利スキャナ・オブジェクト」となる。オブジェクト指向において役割の分割は肝要な概念なので,データ構造と処理の分離方法として一見の価値はある。ただ,ここまでやらなくても十分「仕事しながらスキャン」できるのでVisitorを実際に使う必要はない,という場合もある。
(分類4)メリットがわかりづらく,使われづらいもの:
デザインパターンの「日蔭組」である。
活用シーンが無いわけではない。なので,窓際族とまでは言わない。
- Prototype: コピーを渡す
- 派生オブジェクト達をあらかじめ生成して陳列しておいて,必要な時にコピーを渡す。派生オブジェクトを動的に管理(追加・削除)できる点がメリット。(もし派生オブジェクトがサブクラスの形式で提供されていたらそうはいかない。)
- 浅いコピーのせいでトラブルが起きたら…?とか,グラフィックエディタを作る機会なんて無いよとか,おまけにPrototypeっていう名前がつくものは他にも色々あるしなど,各種障壁のために,少々手を出しづらく影が薄くなるパターン。
- Abstract Factory: 工場の工場
- 工場は,命令に応じて様々なプロダクトを動的に作る。でも,だんだん工場のバリエーションが増えてくる。そこで,工場自体を動的に生成して,さまざまな工場のバリエーションを持たせる。誤解を恐れずに一言で言い表すと,FactoryのFactory。工場の工場。
- 要はFactory Methodパターンをさらに上から抽象化したものなので,FactoryとAbstractを組み合わせるとこうなるのね,という応用的な位置付けになる。
- Flyweight: キャッシュ付の工場
- このパターンは,インスタンス生成の負荷を減らすために,既存のオブジェクトを使いまわす事によってパフォーマンスを向上させる。という点では,SingletonパターンやPrototypeパターンと同じ。ただしインスタンス生成のタイミングとか,各インスタンスの共有の程度が異なっていて,Flyweightパターンの場合は実現すると「Factoryにキャッシュ機能が付いたような物」ができあがる。特定のタイプのインスタンスをくれ,と言われた時点でそのインスタンスを影ながら生成し,あとあと使いまわすという仕組みなので。SingletonとPrototypeの中間ぐらいに位置するパターンと言えるかもしれない。
- Flyweightの発想自体は,プログラミングを進めていけば,自然な発想としていつの間にか使っているというケースがある。FactoryとSingletonを知っていて,Factoryの裏側でSingleton化対象がいくつもあるような場合は,自然とこういう設計になる。そういう意味では,「(分類1)言われなくても自然に使うもの」に入る,と考える事もできる。
- だが,このパターン名自体が余りにもマイナーだ。それに,よく考えると,Flyweight自体がもともと提唱している設計をそのまま流用できるシチュエーションは以外と少ない。オブジェクトの生成コストが大きくて,なおかつインスタンスが「そのままで」使い回しが効く必要があるのだ。普通,使いまわされる側は多少なりとも個別にカスタマイズされるので,全く同一のインスタンスでは困る。なので,このパターンはよく考えると影が薄い方に分類されるのかもしれない。
- あるいは,このパターンは実はアンチパターンなのかもと割り切って考える事もできる。オブジェクト生成のコストを削減するために,Factory役にキャッシュ機構を持たせようという発想自体はごく自然だ。だが,プリミティブ型ではないものをキャッシュしたらそれは値ではなく参照の保持なわけで,もし参照「先」で変更が発生したら全ての参照「元」に影響が及んでしまう。だからキャッシュ機構を作る際には用心しなさいよ。という,よくある警告を伝えるだけのために,このFlyweightパターンは存在する,と考えてもいいかもしれない。
以上で,全23パターンの要約を終える。
全パターンを「俯瞰」できたのでは?
大事なことは,何か?
これらのパターンの中には,重要で有用な物もあれば,そうでないものもある。
成長のために一読が必須なのもあるし,当たり前すぎて学び直す価値があまりない物もある。
だから,23個全部のパターンをキッチリやる必要はないのだ。
1個1個の設計パターンが「絶対」ではないから。
大事なのは・・・
- (分類1)のレベルの設計は,言われなくても自然に思いつくような,そういうセンスを身につけること。
- (分類2)のレベルの設計は,いちいち勉強しなくても「どっかで見たぞ」と言えるぐらいの,経験を積むこと。
- (分類3)のレベルの設計は,意図的に学習するための時間と労力を割くこと。あと,この段階をしっかりと理解できる程度までの技術力を,ちゃんと身につけること。
- (分類4)のレベルの設計は,他のパターンが分かれば別にいらないなあ,と自分自身で察知できる程度まで,他のパターンをよく理解すること。
という事になる。
そこまで来れば,次のステップを目指せるのだ。
学んだパターンを実践する。必要性を見極めて,適切なタイミングで採用する。
オリジナルのクラス構造を意識しつつも,自分なりにアイデアを盛り込んでカスタマイズして,実際に手を動かして実装してみる。
そうすれば,設計力・実装力が飛躍的に増す。
また,デザインパターンの次の段階として「Effective Java」などの必読書を読めるようになる。
(逆に言うと,デザインパターンごときの段階でつまずいていると,各種必読書を読むための力がつかない。)
デザインパターンは,エンジニアに対して,先々のいろんな扉を開いてくれるのだ。
独学用のリンク集
デザインパターンの具体的なサンプルコードや,個々のパターンに関する詳しい解説は,下記のリンク集から取得可能。
ぶあつい入門書を買わなくても,やる気さえあれば,デザインパターンは無料で習得できる。
※とはいえ,古本でも構わないので,手元にリファレンスとして紙媒体があった方が良い。(本エントリを印刷してもいいけど。)
それに,いわゆる「名著」は将来的に読破・制覇した方がよい。
入門者向けのパターン一覧表:
サルでもわかる 逆引きデザインパターン / デザインパターンとは
http://www.nulab.co.jp/designPatterns...
- 名前と概要の一覧表
- メリットは,再利用性の高い設計,コミュニケーション時の共通言語,オブジェクト指向の理解
Deep Side of Java〜Java 言語再入門 第3回 〜 クラス設計とデザインパターン
http://www.nurs.or.jp/~sug/soft/java/...
- 名前と概要と適用例の一覧表
矢沢久雄の早わかりGoFデザインパターン
http://itpro.nikkeibp.co.jp/article/C...
- 名前と,名前の直訳と,解説記事へのリンク
- 「バブルソートや二分探索といったいわゆる「定番アルゴリズム」のようなもの。知ればプログラミングに目覚める」
Java プログラマのためのデザインパターン入門
http://objectclub.jp/technicaldoc/pat...
中級者向けのパターン一覧表:
デザインパターンを読み解く(おすすめ)
http://www.happiese.com/system/dpatte...
- 分類方法に関する鋭い考察と,まとめ。紹介・解説だけでなくコメント・批判つき
- 「クラス設計の上位レベルのコンセプトと、コーディングのテクニックをごっちゃにしたようなもの」
- 「大方は普通にクラス設計してくれば出てくるもので,あえてパターンと名づけるほどのものではない。パターンというよりテクニックやイディオム」
- デザインパターン全体を通して強調されているのは、インターフェース(やAbstract)でプログラミングするということ
全てのパターンを,Javaのサンプルコード付きで独学できるページ:
デザインパターン INDEX
http://www.techscore.com/tech/DesignP...
- 練習問題と回答とUMLダイアグラムつき
デザインパターン入門(C#,Java サンプルコード)、UMLで紹介
http://www.rarestyle.net/main/pattern...
デザインパターンの骸骨たち (RE-BONE) with C
http://www002.upp.so-net.ne.jp/ys_oot...
矢沢久雄の早わかりGoFデザインパターン
http://itpro.nikkeibp.co.jp/article/C...
- プログラム全体のコードが載っているわけではなく,要所のみを数行でさらっと解説
ときに,「オブジェクト脳のつくり方―Java・UML・EJBをマスターするための究極の基礎講座」("オブ脳")という書籍を本屋で見た事のある人も多いはず。
あの本は,ちょっと吹っ飛んだ入門書なのだが,23あるデザインパターンの中で最も重要なのは下記の5つだ。 としている。
- Template Method (本エントリでは分類1)
- Factory Method ( 〃 分類2)
- Composite ( 〃 分類3)
- State/Strategy ( 〃 分類3)
※4章の「先人は偉い、超手抜きパターン習得法」を参照。
著者の牛尾 剛氏によれば,「他のパターンはこれら5個の応用に過ぎないので,後から手を付ければいい」のだという。
多数あるパターンから要点のみを抽出して,効率的に学習する上で,参考にできる意見だろう。
「マルチスレッド編」のデザインパターン
デザインパターンには「マルチスレッド版」もある,とされている。
これはGoFとは関係なし。
国内での認知は,下記の書籍の貢献が大きい。
増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編 [大型本]
http://www.amazon.co.jp/exec/obidos/A...
- 700ページもある本
その本の旧版の書評
http://manuke.com/review/view.php?f_r...
- 「コンピュータ科学にのっとった美しい解説に感心」
- synchronizedを見つけたら「なにを守っているのか」に着目すると良い、のような実践的な手引きも
Think IT著者陣がおくる デザインパターン必携書籍 厳選6冊
http://thinkit.co.jp/article/941/1
増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編 の感想
http://www.sutosoft.com/room/archives...
- クリティカルセクション等の定石を,Java用のパターン名として言いなおしたもの
- マルチスレッドでの開発が未経験であれば必読書
- 経験者であれば技術的に新たに得る物は少ないかもしれないが,パターン名を共通のボキャブラリとして獲得できるので,マルチスレッドに関する技術的な解説文章を書く時に重宝する。パターン名を提示するだけで説明が済むから。
上述の書評から見てわかるように,もしマルチスレッドに自信がなければ,中古・新品問わず購入が推奨される。(もちろんGoFが終わってからの段階だが)
WIN32API上とか,他のプラットフォームや他の言語でスレッドを十分に扱った経験があったりしても,とりあえず目次の一覧に目を通し,パターン名を「Java用のボキャブラリ」として覚えておいた方がよい。
Web上で独学するためのリソース:
デザインパターン入門 マルチスレッド編まとめ
http://d.hatena.ne.jp/otuzak/20080527...
- 全12パターンの要約メモ
- Single Threaded Execution
- Immutable
- Guarded Suspention
- Balking
- Producer-Consumer
- Read-Write Lock
- Thread-Per-Message
- Worker Thread
- Future
- Two-Phase Termination
- Thread-Specific Strage
- Active Object
ギコ猫とマルチスレッドのパターンたち
http://www.hyuki.com/dp/cat_index.htm...
- 12パターンの概要をAAで学ぶことができる
デザインパターン(マルチスレッド)
http://www.tom.sfc.keio.ac.jp/~fjedi/...
- 12パターンのJavaによるサンプルコードと解説集
Javaのマルチスレッド・プログラミングのリンク集 (デザインパターンの解説集つき)
http://language-and-engineering.hatenablog.jp/entry/20120905/p1
その他のパターン
GoF以外の他のデザインパターンは,下記のサイトで学習できる。
GoF以外のパターンを紹介します
http://www.hyuki.com/dp/dpinfo.html
こういったのを学ぶ際には,各種の「オブジェクト指向の原則」を頭に入れておくと,全体が俯瞰しやすいと思われる。
補足:面談用の一覧表
冒頭の一覧表から,コメントをすべて除去した物を掲載する。
・Factory Method | ・Abstract Factory ・Builder ・Prototype ・Singleton |
・Adapter | ・Adapter ・Bridge ・Composite ・Decorator ・Facade ・Flyweight ・Proxy |
・Interpreter ・Template Method |
・Chain of Responsibility ・Command ・Iterator ・Mediator ・Memento ・Observer ・State ・Strategy ・Visitor |
例えば採用の面談なんかで,
鼻高々に「Javaの設計できますよ」というエンジニアがいたら,上記の表を印刷し
「それはとても良いことですね。じゃ,ここにあるのを全部,それぞれ一言で説明してもらえますか」
と言う。
前述の通り,全部をそらで言える必要はない。
重要なものとそうでないものを区別した,それなりに的を射た回答が返ってくれば,OK。
さらに,自分がよく使うパターンの好き・嫌いが言えれば,なお良し。
もし気まずい表情で,よくわかりませんという回答が返ってくれば,
「じゃあ,かわりに何を使ってオブジェクト指向の設計を勉強したの?」
と質問する。
筋道だてて説得力のある納得のいく回答が返ってくれば,問題なし。
口ごもったら,切り捨て。
関連する記事:
Java使いの必携書「Effective Java 第2版」を,通読・マスター・事後参照するためのリンク集
http://language-and-engineering.hatenablog.jp/entry/20121211/p1
Javaのジェネリクスで,T.class や new T() ができず悩んだ話 (型パラメータのインスタンス化に関し、フレームワーク設計からケーススタディ)
http://language-and-engineering.hatenablog.jp/entry/20120502/p1
Javaのマルチスレッド・プログラミングのリンク集 (デザインパターンの解説集つき)
http://language-and-engineering.hatenablog.jp/entry/20120905/p1
「バリデーション」APIと「単体テスト」APIの類似性,およびそのスタイルが時代と共に洗練される過程の概観
http://language-and-engineering.hatenablog.jp/entry/20120320/p1
Javaの非同期処理を,シングルスレッドのようにシンプルにコーディングするための設計パターン (並列処理を逐次処理にする)
http://language-and-engineering.hatenablog.jp/entry/20120205/p1
グラフィック系アルゴリズムに役立つ「計算幾何学」の入門用ノートPDF (Computational Geometry)
http://language-and-engineering.hatenablog.jp/entry/20140528/ComputationalGeo...
あまり知られていない,Webアプリ開発時の10の略語 (例文つき)
http://language-and-engineering.hatenablog.jp/entry/20101102/p1
*1:英語のアルファベット順で,「A」が最初だからだ。 日本人にもっと配慮してくれ。 だからと言って,五十音順で「ア」から始めればいいって事じゃないぞ