デザパタを一人でこっそり振り返ろう #4 (Factory Method)
なんと前の記事を確認したら一年以上前だよ…… orz
ということで誰も読んでないと思いつつも続きを書きますが、へっぽこプログラマのぼくがひょんなきっかけから、Javaプログラマ向けデザインパターンの入門書として有名な:
- 作者: 結城浩
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2004/06/19
- メディア: 大型本
- 購入: 51人 クリック: 762回
- この商品を含むブログ (400件) を見る
Design Patterns Smalltalk Companion, The (Software Patterns Series)
- 作者: Sherman Brown, Kyle Woolf, Bobby Alpert
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 1998/02/10
- メディア: ペーパーバック
- 購入: 1人 クリック: 23回
- この商品を含むブログ (9件) を見る
過去の記事は次のとおり*1。
- デザパタを一人でこっそり振り返ろう #1 (Iterator)
- デザパタを一人でこっそり振り返ろう #2 (Adapter)
- デザパタを一人でこっそり振り返ろう #3 (Template Method)
ということで今回は Factory Method パターン。JDP から引用すると(p.46)、
Template Method パターン (中略) をインスタンス生成の場面に適用したもの
なんだそーです。
リフレクションとかの機能がなくて、動的にクラスにメソッドを追加できなくて、文字列をもとにオブジェクトを操作するみたいな作業がニガテな C++ や Java に比べると、Smalltalk や Ruby なんかだとあんまり大げさなことやらなくてもいい気がするんですが、はてどうでしょう。
ひとまず実践
えーと、JDP で挙げられている例は、「IDカード」を抽象化したクラスを作るためのフレームワークを提供するってものです。入門書って性格上しょうがないと思うんですが、JDP の例はこういう「例のための例」が多くてちょっと歯がゆいです。
まあ文句をいってないで実装しましょう。
JDP では Java なのでパッケージ機能を使って、framework パッケージと idcard パッケージに分けてますが、これは Smalltalk ならカテゴリーにしましょうか。framework はクラスを「作る」「使う」という抽象概念を定義し、idcard はそれを具象化するパッケージというわけ。
まずは framework 側。カテゴリー名は当たらないように長ーくなってます。通常の Smalltalk の名前付けルールとは違うけど許して。
んで具象側のオブジェクト側はこんな感じ。
出力は手抜きで Transcript に出すので、Transcript を表示させておいて:
hoge := IDCardFactory new. pro1 := hoge createOf: 'なるひこ'. pro2 := hoge createOf: 'なるおが'. pro3 := hoge createOf: 'naru0ga'. pro1 use. pro2 use. pro3 use.
とかやると、
なるひこのカードを作ります。 なるおがのカードを作ります。 naru0gaのカードを作ります。 なるひこのカードを使います。 なるおがのカードを使います。 naru0gaのカードを使います。
てーな感じになるわけです。
super new って大丈夫なん?
で、これを書くときに意外とハマったのが、コンストラクタで値を初期化するよーな書き方。って、今までやったことなかったんかいな自分……。
具体的には、
IDCardFactory class>>new ^ super new initialize. IDCardFactory>>initialize owners := Bag new.
あたりですね。
C++ や Java、Ruby なんかだとコンストラクタでメンバー変数代入すればいいんで (C++ だと初期化子を使うほうが行儀がいいのか?) 最初は:
IDCardFactory class>>new owners := Bag new.
って書いたら怒られた。「クラスインスタンス変数には owners なんてねーよ」って。おうふ。
じゃあってんでインスタンス生成のときに値を初期化しているようなオブジェクトなんかねーかなって考えて、そうだ Complex とかどうしてるんだろって見たら、
Complex class>>new ^ self real: 0 imaginary: 0. Complex class>>real: aNumber1 imaginary: aNumber2 | newComplex | newComplex := super new. newComplex real: aNumber1; imaginary: aNumber2. ^ newComplex.
みたいになってる。ここで real: と imaginary: はどっちも Complex で定義されてるメソッド。
んー? super new したものに Complex のメッセージ投げて、なんでうまく動くんだろう?? と思って、ついったに:
Hoge>>new ^super new initialize. とかやってちゃんと動くのが不思議な感じがする。super newしたから親クラスのオブジェクトができてるはずなのになんでinitializeメッセージが投げられるんだろうと不思議な感じ
ということは、super new して得られるオブジェクトはあくまでも super の持つオブジェクト (= IDCardFactory クラス) のインスタンスオブジェクトなんだけど、new を処理するのはその上位 (= Factory -> Object -> ...) ってーことなんやね。なんかややこしいけど、イディオムとして覚えとこ。
まあ結果としては、今回の書き方で正しいらしいです。
このあとの議論も面白かったし、ここ深堀するといろいろ面白そうだけど、主題じゃないのでまたいつの日か。
考察的な何か
で、えーと、とりあえずこの例だと:
- Factory の時点で「作るために必要なもの (Factory class>>createFor: anOwner の anOwner)」が決まってしまっているのは果たしてどうなのか。ものすごく使いにくいような気がする
- なんで Register しているのか説明がなんにもないので意味がわかんない。これはこのパターンに必要なのか?
とか思っちゃうわけですよね。
後者のほうはどっちかというとテキストの問題ですな。IDCardFactory クラスで register するのは多分、作ったオブジェクトに対してブロードキャストとか処理の委譲をしたいからだと思うんだけど、JDP だとそういう話はぜんぜん出てこないし、そもそもそういうことしたいなら具象クラスじゃなくて抽象クラスの Factory の方でやらないとマズイ気がするし、教科書としては不要な例じゃないかなーと思うのだけど、まあ、先を読み進むと意味があるのかもしれませんし、そこはあまり追求しますまい。
むしろ前者が辛いですよねー。これって実用できる範囲をとても狭めてしまうと思うんだけど……そこのところ、もうちょっと解説が欲しいすな。例えばウィジェットの生成なんかは Factory をよく使う例だと思うんだけど、ウィジェットの場合、生成時に必要な初期化パラメータって微妙に異なったりしますよね。そういう場合、このパターンでやるのが正解なのか、違うパターンでやるのがいいのかとか、もし Factory Method でやるとしたら Java の場合にはどう実装するのが正解なのかってのが知りたいなーと。
DPSC を読んでみた
さて DPSC の Factory Method パターンは p.63 から。
前にも書いたけど DPSC の場合は「このパターンは Smalltalk のクラスライブラリの中で実際こう使われてるよ」って書いてあることで、ここで例に出されているのはズバリ、MVC (Model-View-Controller)。MVC の元祖は Smalltalk なんですよね。でまぁ、例として挙がってるのは複数の種類 (Text と Drawing) の Document を扱える Editor アプリケーション。これは Editor を Factory、Document を Product として考えれば実現できますよと。
Factory Method で MVC のクラスの管理コストを下げる
MVC の辛いところは、要素が増えてくるととにかくクラスの量が爆発的に増えることですよね。DPSC で挙げられているのは View と Controller の関係なのですが、基本的には View は「違う見え方」に対して一つずつクラスを作るのに対して、Controller は対応するだけ作る必要はなくて上位のビューと共通化できることも多いです (Smalltalk の例であれば View の BooleanWidgetView > ActionButton に対し、Controller は両方とも WidgetController でよい)。となると、両者の「どのビューはどのコントローラを使うか」っていう管理をしないといけないです。MVC の元祖的な Smalltalk-80 の場合は Dictionary で管理してたらしいですが、これはクラスが増えるたびに管理が大変。
ということでこの点について VisualWorks で使っているテクニックが紹介されてます。さっくり引用(p.68)。
View>>getController "レシーバーの現在のコントローラーを返す。もしレシーバーのコントローラーが nil だったら (通常の場合)、レシーバーのデフォルトコントローラーの初期化されたインスタンスが導入され、 そのインスタンスを返す。" controller == nil ifTrue: [self setController: self defaultController]. ^controller.
んで、defaultController を Factory Method としてみなせば、それぞれの View に対応して defaultController の定義をこんなふうに書けばよくなる。
View>>defaultController ^Controller new. Text>>defaultController ^TextController new.
しかしもうひと工夫。どちらの defaultController も「あるクラスに対して new を投げてるだけ」なんだから、これも括り出しちゃえばいいじゃないかと。そんなわけでこんな感じ。
View>>defaultController ^defaultControllerClass new. View>>defaultControllerClass "レシーバーのデフォルトコントローラーのクラスを返す。" "もし Controller 以外のコントローラを使いたければ、サブクラスは" "defaultControllerClass を再定義すること。" ^Controller. Text>>defaultControllerClass ^TextController.
……正直、きれいになったとは思うけど、うれしさがさっきの例とくらべてよくわからない自分がいます……。
あ、でも下位クラスでオーパーライドすべきメソッド (逆にいえば、同じコントローラで構わなければオーバーライドしなくてもいい) がわかりやすいのはいいですね。
ここでの defaultControllerClass みたいなメソッドは Constant Method と呼ばれるらしくて Kent Beck の:
ケント・ベックのSmalltalkベストプラクティス・パターン―シンプル・デザインへの宝石集
- 作者: ケントベック,Kent Beck,梅沢真史,皆川誠,小黒直樹,森島みどり
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2003/03
- メディア: 単行本
- 購入: 7人 クリック: 94回
- この商品を含むブログ (55件) を見る
それから、同様の方法はクラスインスタンス変数を用いてもできますよねという話。さっきの例を書き換えればこんな感じかな?
View class instanceVariableNames: 'defaultControllerClass'. View>>defaultController ^self class defaultControllerClass new. View class>>initialize defaultContorllerClass := Controller. TextView class>>initialize defaultContorllerClass := TextController.
あと p.73 からの「Known Smalltalk Uses」で、豊富な実例が紹介されていてなかなか素敵なのだけども省略。クラス生成だけじゃなくて「外部 (external)」のなにかを呼び出すときに、呼び出すパラメータ文字列を生成する奴なんかも Factory Method パターンで扱えるよってのはふむふむだった。
まとめ
さっきの疑問「インスタンス生成のときに決まった引数を渡さなきゃいけないのって、なんかうれしさがわからない」という件、DPSC では「ただ new するだけ (引数とか取らない) でも嬉しいパターンはたくさんある」ということを提示していて、これなら便利さが朧気ながら分かりますね。MVC の例のように、「あるクラスと関係するクラスを new して関連を保持しておく」みたいな処理はみんなこのパターンになるわけだから。むしろ「工場と製品」アナロジーから離れたほうが広く応用が考えられそうですね。
次回は Singleton。いつになるやらわかりませんが(^^;
*1:タイトルが一貫してないことが発覚したのでこそこそ修正。