Mar 29, 2011

StrategyパターンとVisitorパターンの使い分けに関する考察(1)

今更ながら、かの著名なGoFのデザインパターンを復習している。

10年前に居た会社にGoF本の日本語版があり、たまに何とかパターンという言葉を使う人が居たので、その本を一通り流し読みしたことがあるが、その時はそれの重要性に気付かず、23種類のパターンのそれぞれについてなんとなくわかった気になったら、1行に要約して控えておいただけだった。
結局、その部署で耳にしたパターン名は「オブザーバーパターン」だけだったので、それ以外のパターンは全て忘れてしまった。

Observerパターンは要するに状態変化時のコールバック関数を登録する設計のことであり、特にGUIのイベントハンドラーを定義する仕組みでは頻出なので、忘れようが無いし、「コールバックモデル」とか「イベントハンドラー」とか、Javaなら「リスナー」とか言えば通じるので、もはや「オブザーバーパターン」という言葉を耳にすることも無い。もしソフトウェア開発の場で耳にしたら、何をカッコつけとんねん、と言いたくなるだろうし、「何かあったら電話して」と言われた時に「オブザーバーパターンですね」と答えるような用途は考えられないことは無いが、よほどのマニアでない限り、その洒落が通じることは無いであろう。

その後、デザインパターンへの興味を失ってしまったのだが、オブジェクト指向とは何かということに興味を持ち、時々図書館でそれ系の本を借りて読むようになると、GoFのデザインパターンがよく引用されているので、また目にするようになった。
というか、GoFのデザインパターン以外に、これぞオブジェクト指向設計と思うような例はほとんど目にしない。オブジェクト指向設計になっていても、実際に動作するアプリケーションになっていると、そのアプリが提供する機能に目を奪われてしまうからだろうか。

そこで、もう一度GoFのデザインパターンを復習した。

半年かかった。

23種類ってのは、1つ1つ取り組むと、思ったより多かった。
当初は1つ1つのパターンについてオリジナルなサンプルコードを作ることを目標としていたが、結構大変であることに気付いたので、方針変更して、興味を持ったテーマについてだけ書くことにした。


今回は、StrategyパターンとVisitorパターンの使い分けについて考える。全然違うもののようだが、特定の処理を交換可能にするという用途に限ると、似たような効果が得られるようである。

Strategyパターンとは、特定の処理を外部のクラスに置くことにより、既存のクラスを変更せずにアルゴリズムを交換可能にするパターンである。次のUML図は、StableClass以下を変更せずに、合計の計算方法を変更することを可能にする、Strategyパターンの適用例である。

これをJavaで実装したサンプルコードを、下の方に添付している。
StableClassを、計算対象のデータを保持する、変更不可なクラスとする。
StableClassが抽象的なSumStrategyへの参照を持っており、合計の計算はそれを介して行われる。具体的なSumStrategyは実行時に決まるので、StableClassを変更することなく、合計の計算方法を追加することができる。合計の計算方法として、通常の足し算でなく、掛け算を選択することができるように変更するには、新たなSumStrategyであるMultiplicationクラスを追加するだけで良い。

Visitorパターンとは、任意の処理を持つオブジェクトを受け付ける口を設けることにより、既存のクラスを変更せずに、様々な処理を後から追加できるようにするパターンである。
次のUML図は、上と同じことをVisitorパターンでやってみた例である。

Visitorパターンでは、visitorが操作対象となるクラス(以下visitee)をvisitすることをそのクラスがacceptするという表現が用いられる。
操作対象のクラスは、visitorのvisitメソッドを呼ぶだけの、acceptメソッドを用意する。抽象的なVisitorが提供するI/Fが呼ばれると、visitorはvisiteeの具体的なクラスを知らないまま、自身を引数にしてaccept()を呼び、visiteeのaccept()は、visitorの具体的なクラスを知らずに自身を引数にしてvisit()を呼ぶ。(accept()の呼び出し時にStableClassの具体的なクラスの特定が行われ、visit()の呼び出し時にVisitorの具体的なクラスの特定が行われる)
このdouble dispatchと呼ばれる2段階の呼び出しを経由することにより、StableClassとVisitorの結合は呼び出し時まで抽象化される。つまり、visitorとvisiteeのクラスの組み合わせによって決まる、実際に実行される処理は、実行時まで未確定にすることができるので、StableClassを変更せずに、新たなVisitorを追加できるのである。
従って、これによっても、新たな合計の計算方法を追加するには、図中のMultipleのようなクラスを追加するだけで良い。

一般的にはVisitorはVisiteeの全ての具象クラスをvisitできる必要は無いとされるため、上図のように、抽象Visiteeクラスにaccept()が無く、VisiteeからVisitorへの関連は、accept()を持つ具象Visiteeクラスから抽象Visitorへの関連とされる(accept()を持たない具象クラスからの関連は無い)ようである(GoF本は手元に無いので未確認)。しかし、今回のように、デフォルトの処理がVisitorとして存在する場合は、全てのVisiteeはvisitableであるとしても問題無さそうなので、StableClassにaccept()を設けてみる。そうすると、StableClassからVisitorへの関連は抽象クラス同士になり、Strategyパターンに近い構図になる。

これをJavaで書いたコードを、下の方に貼っている。

本題の考察に入る前に、もう少し大きなサンプルを作ってみる。
(次のエントリーに続く)

See more ...

Posted at 23:30 in PC一般 | WriteBacks (3)
WriteBacks

Finally! This is just what I was looknig for.[link]

Posted by Jahlin at 07/21/2011 02:30:04 AM

かなり古い記事ですが読んでいて気になったので。 visitorパターンの SumVisitorインターフェースが抽象化されていないです。 StableClassCが登場した場合に、インターフェースとすべての具現クラス(Concrete)に変更が発生します。 各visitor内で、instanceofを使って具象クラスの増減を吸収するのが骨子かと思います。 StrategyとVisitorの違いですが、呼び出す側から見て、 ・新しい処理の追加に強いのがVisitor  従ってvisitorは訪問先の構造を知らなくてはならない。状態も持つことが多いはず。  (例:エクスローラのデータサイズ合計の表示、グラフ表示のプラグインなど) ・構造が殆ど同じで、一部処理を切り替えるのがStrategy  状態を持たない場合もあるでしょう。  (例:データベースとの接続で、MS Access/Oracle/My SQLなど) ではないでしょうか?

Posted by at 02/19/2013 05:31:28 AM

鋭いコメントありがとうございます。 SumVisitorが、全てのStableClassの具象クラスについて、それを引数として受けるためのメソッドを定義するべきかどうかというのは、確かに悩みました。実は私も、最初はSumVisitorのvisitの引数の型を抽象StableClassにしていました。その方が楽だからです。 おっしゃる通り、そのようにして、具象クラスのvisitメソッドにてinstanceofを使ってどのStableClassの具象クラスかで場合分けをするようにすれば、StableClassCが現れてもSumVisitor以下全てに改造が発生することになりません。 しかし、いわゆるGoF本では、Visitorインターフェースは全ての具象Visiteeクラスについてvisitメソッドを定義しています(参考リンク1)ので、ここではそれに従いました。 実はVisitorパターンの目的の1つは、そのような場合分けによるvisitメソッドの複雑化を回避することにあります(参考リンク2)。オブジェクト指向設計は元々、できるだけ条件分岐はクラス分け(データの特徴付け)やメソッドのオーバーロード(協調相手の特徴付け)によって解決しようとする考え方ですので、それを追求した結果、VisitorパターンではVisitorインターフェースでVisiteeの具象クラスを解決するようになっているのだと思います。 実用的かどうかは別にして、VisitorインターフェースにおけるVisitor側の具象クラスの解決は、Visitorパターンの定義の一部だと考えて良いと思います。また、VisitorパターンではVisiteeが主でVisitorが従で、Visitee側は不変(なので"Stable"というクラス名にしました)だという前提があり、もしVisiteeに追加変更があれば全てのVisitorに変更が及ぶというのはVisitorパターンの宿命と考えて良いと思います。 なお、acceptメソッドをVisitableインターフェースと定義し、visitメソッドにて、VisiteeがVisitableかどうかをinstanceofで調べるのは、Visitorパターンの発展形として知られているようです。この用途でinstanceofを使用するのはVisitorパターンの範囲内だと思いますし、そういう選択肢があることはVisitorパターンを適用する上で必須の知識だと思いますが、VisitableでないStableClassはVisitorパターンに当てはまらないので、本記事では省きました。 後半のStrategyとVisitorの違いについては、全くおっしゃる通りだと思います。[link]

Posted by ynomura at 02/25/2013 08:28:42 PM