タグ「GoF」が付けられているもの

これまで[1][2]にStrategyパターンとVisitorパターンそれぞれで作った引力モデルのシミュレーションに対して、以下のような要素を追加してみる。

  1. 物体の種類として、Square
    Squareの特徴:
    • 縦か横にしか動かない(縦方向か横方向かどちらか速度の大きい方に動く)
    • 但し速度は2次元(一方が空回り)で、全方向の引力の作用を受ける
    • 標準の引力は通常の距離の2乗に反比例する引力
    • 標準色は緑
  2. 引力計算の種類に、引力の方向が相手の方向より少しずれるCurlGravityを追加する
  3. 色設定の種類に、Blinkを追加する
それぞれ、PhysicalObject, GravityStrategy/Visitor, ColorStrategy/Visitorのクラス階層へ何かを追加するという例である。

Strategyパターンによるものに対する変更は、次のようになる。
class diagram
元のクラス図に対して、黄色で示されたBlink, Square, CurlGravityの3つのクラスを追加している。

これに基づいて変更したソースコードとJavaアプレットを、次のリンク先のページに置く。
・StrategySample3のアプレットとソースコードのページ
ソースコードの差分は、詳しいdiffの出力を末尾に載せているが、 Square.java, CurlGravity.java, Blink.java の3ファイルが新規に作られた他は、変更されたファイルは SampleApp.java の1つのみであり、その変更も、新たに追加したSquare, CurlGravity, Blinkの3つが選択できるようにGUIのボタンを追加したりボタンの動作を変更したりするものだけであり、それらを使わないアプリケーションには必要の無い変更である。
つまり、本体側のサブクラスの追加もStrategyの追加も、既存のクラスに全く手を入れることなく行うことができる(†追記部分に補足あり)ということであり、いずれの場合も、オブジェクト指向設計の基本であるであるOpen-Closed Principleを満たしていることが明らかである。

一方、Visitorパターンによるものに対する変更はかなり多い。
class diagram
元のクラス図に対して、黄色で示されたBlink, Square, CurlGravityの3つのクラスを追加した他に、Visitorの階層のGravityVisitor, DefaultColorizerを除く全ての既存のクラスに変更が入っている。

これは、Visitorインターフェースにvisit(Square)を追加した為、Visitorインターフェースを実装する全てのクラスにvisit(Square)を追加する必要があるからである。もちろんVisitor階層で実装を継承できる場合は追加不要だが、Visitorパターンがその効果を発揮する用途ではVisitor同士は役割が類似しないことが多い(後述)というよりむしろ全く異なることが多い為、Visitorインターフェースが変更されても変更せずに済むのは、親クラスを僅かに拡張をするようなクラスに限られる。
また、Visitorインターフェースにvisit(Square)を追加しなくても良いのは、SquareがVisitorを一切acceptしない(accept()が何もしないか例外を投げる)場合のみである。(accept()でのvisit(this)はコンパイルエラーになる)

これに基づいて変更したソースコードとJavaアプレットを、次のリンク先のページに置く。
・VisitorSample3のアプレットとソースコードのページ
ソースコードの差分は、同様にdiffの出力を末尾に載せているが、 Square.java, CurlGravity.java, Blink.java の3ファイルが新規に作られた他に、 SampleApp.java, ColorVisitor.java, DefaultGravityApplier.java, Gradation.java, NegativeColorizer.java, NormalGravityApplier.java, RepulsionApplier.java, Visitor.java の8つのファイルが変更されている。
SampleApp.javaの変更は、Strategyパターンの場合と同様、使用者側が新たなクラスを使う為の変更であるが、その他は既に実装されているメソッドをcopy&pasteしたような怠惰な追加である。末尾のdiffの出力は先頭が+の行が追加された行であり、変更箇所の前後3行が参考の為に表示されているが、SampleApp.javaを除く前述の7ファイルの追加内容は、それらのすぐ上に同一または類似の処理があることが見て取れると思う。いかにも冗長である。

ただ、これら7ファイルは全て、PhysicalObjectの階層にSquareクラスが追加されたことに対応してvisit(Square)が追加されたものであり、CurlGravityやBlinkの追加に伴って発生した変更ではない。よって、VisitorパターンはVisitorの追加に対しては変更箇所が閉じている(Open-Closed Principleを満たしている)ことがわかる。
繰り返しになるが、visit(Square)を追加しなくて良いのはSquareがVisitorをacceptしない場合のみであり、それはVisitorパターンの適用外ともいうべきもので、そういう場合には変更に閉じていると言えるものではない。

以上より、StrategyパターンとVisitorバターンの違いとしては、次のようなことが見られる。

  • Visitorパターンの場合、本体側のクラス階層にサブクラスが追加されると、Visitor階層全体に変更が及ぶ。
    Visitorパターンの宿命のようである。
  • Visitorパターンでは、いずれのVisitorクラスも、本体側の全ての具象クラスについて別々のメソッドを持つ。
    具象クラス毎に処理が異なることが少ない場合は、適しているとは言い難い。一方、Strategyパターンでは、具象クラス毎に処理が異なるStrategyのみ、具象クラス毎にStrategyのクラスを別にすることが可能である。
  • Visitorパターンでは、本体側のクラスが主体となってその処理を実行することができない。
    accept()が呼ばれるまではVisitorが実装する処理を実行できない。その為、例えばメソッドの途中でVisitor側にある処理を呼ぶことは困難である。(抽象Visitorクラスがイベント発行要求を受け付けて、Visitorインスタンスを管理するクラスに通知して、acceptを呼ぶような、トリプルディスパッチが必要かも知れない)
  • Strategyパターンが本体側のクラスのインスタンス毎にStrategyを持つのに対して、Visitorパターンは特定のインスタンスとの関連が無い。
    切替可能な処理を繰り返し実行するのであれば、Visitorパターンの場合はVisitorのインスタンスを別に管理する必要が生じる。
    切替可能な処理を1回だけ、複数のオブジェクトに対して実行するのであれば、Strategyパターンの方が処理が煩雑になる。全オブジェクトに(抽象化された)同じ処理を1回だけ適用したい場合には、Visitorパターンが有利である。
  • Strategyパターンの場合は、Strategyの使われ方が決まっているので、追加できるStrategyの自由度は限定的だが、Visitorパターンの場合は任意と言うに近い自由度で処理が追加できる。

そもそも、Visitorパターンが有効な場合は相当限られると思う。
適用されるクラス群は抽象化されているのだからそれなりに共通の特徴を持つだろう が、
・Visitorから見るとクラス毎に処理が違うくらいにはサブクラスは異なる特徴を持ち、
・Visitorが行う抽象的な処理の種類はStrategyとして共通の特徴を持たないくらいには異なる、
ような場合ということになる。筆者がこれまでに見たことがある例は

  • オブジェクトダンプのようなデバッグ表示(toString+printのようなもの)
  • ファイル保存などのための、オブジェクトのフォーマッティング
くらいであるが、他にはなかなか思い付かない。ファイルハンドルのように、ストレージはファイルシステムかHDDかデータベースかネットワーク越しの何かかわからないようなものに対する処理を追加できるようにしておくような場合だろうか。

前のエントリーでStrategyパターンで作った引力モデルのシミュレーションとほぼ同じものを、今度はVisitorパターンで作ってみる。


class diagram

Strategyパターンの場合との全体的な構造上の違いとして、本体のクラスに切替可能な処理(またはアルゴリズム)への関連が無く、処理を実装するクラスから本体のクラス階層への関連がある。具体的には、PhysicalObjectクラスにVisitorのメンバが無く、Visitorの具象クラスから本体の具象クラスへの依存がある。

切替可能な処理の構造の違いとしては、Strategyパターンでは本体の具象クラス毎の既定の処理をStrategyでなく本体側に入れることが可能であったのに対して、VisitorパターンではそれらをVisitor側に定義する必要がある。もし本体側に実装しても、それを呼び出す仕組みが必要になるので、Visitor側にクラスを定義するのは避けられない。
また、Strategyパターンでは処理が無い場合は無処理のStrategyを定義する必要があった(特にStrategyが値を返す場合)が、Visitorパターンでは無処理ならVisitorを定義する必要が無い。
具体的には、各PhysicalObjectの標準の処理の実装のために、DefaultGravity, DefaultColorizerというクラスを定義しており、無重力の場合は引力計算の処理の必要が無いので、NoGravityApplierはのようなクラスを定義していない。

なお、ColorVisitorのサブクラスにて各PhysicalObjectの標準色を使う場合があるため、
標準色の設定処理はColorVisitorクラスに実装するようにし、DefaultColorizerは、ColorVisitorをそのまま引き継ぐ、インスタンス生成可能にするためだけの空のクラスとしている。ColorVisitorを抽象クラスでなく具象クラスにすればDefaultColorizerクラスが不要になるように思えるが、そうすると、ColorVisitorが標準色の設定の役割を兼ね、ColorVisitorのサブクラスが標準色の設定の役割を持つColorVisitorとして振る舞えることになってしまい、いわゆるリスコフの置換原則に反してしまうので、好ましくない。

以上の方針で実装したのが、下のリンク先のページのJavaアプレットである。(ソースコードへのリンクもあり)
・VisitorSample2のアプレットとソースコードのページ

StrategyパターンのサンプルではボタンによるStrategyの切替がそれ以後に追加される物体のみに有効になるのに対し、Visitorパターンのサンプルでは、フィールド上にある全ての物体に対して切り替わる。
WiredStarについては、同じVisitorでもvisit先の具象クラスによって処理を変更する例として、全てのGravityVisitorのvisit()を空にし、常に無重力の状態にしている。

オブジェクト毎に別々のVisitorを適用するようにしていないのは、そうするためにはアプリケーション側でオブジェクトとVisitorとの対応を管理しないといけなくなり、Visitorパターンのメリットが大きく損なわれるからである。

次のエントリーで、StrategyパターンとVisitorパターンの例に対して、本体と切替可能処理のそれぞれのクラス階層にクラスを追加してみる。(続く)

StrategyパターンとVisitorパターンの応用例として、簡単な引力モデルのシミュレーションを作ってみる。引力の計算方法は外部クラスに存在させ、既存クラスを変更せずに追加/変更可能にする。
なるべく簡単にするため、次のような制限をつける。
・全物体の質量は同じ
・空間は2次元
・引力は2物体間の距離だけで決まる

まず、Strategyパターンを応用する例を考える。
引力の計算方法についてStrategyパターンを適用し、クラス構成は、次のようにする。

class diagram

PhysicalObjectクラスの階層(緑部分)が物体の位置や速度を持ち、GravityStrategyクラス/インターフェースの階層(水色)が引力の計算処理を持つ。抽象レベルでPhysicalObjectがGravityStrategyに関連づけるため、PhysicalObjectの基底クラスがGravitystrategyへの参照を持つ。
NormalGravityは距離の2乗に反比例する引力、Repulsionは距離の2乗に反比例する斥力、NoGravityは引力なしである。

GravityStrategyが定義するメソッドは、計算結果を返すようにする方が自然だが、今回の例では、そのまま計算結果をPhysicalObjectに反映するようにしている。これは、Visitorとの類似性を追究する目的のため、そのようにした。その為、 PhysicalObjectの値を更新する為のaccessor(VelocityControllerインターフェース)が必要になっている。

Strategyパターンとして、そういう実現方法は許されるのかという問題があるが、一般に計算結果が1つの値とは限らないし、Strategyに委譲する処理が複雑になるほど、戻り側で必要な値が増える可能性があるし、出力する値の個数が決まっていると拡張する範囲が限られてしまう。
また、入力としてオブジェクト丸ごとでなく、最低限のパラメーターだけを渡す方がいいという考え方もあるが、やはりそのパラメーターの数が決まっていると拡張性が制限されてしまう可能性がある。
それに、入力のデータ型を新たに定義せずに委譲元のクラスそのものとするのであれば、出力のデータ型についてだけ、戻り値を格納できる新たなデータ型を定義するのはすっきりしないし、一般にStrategyの計算結果は中間データなので、委譲元のクラスには格納できない。
よって、Strategyへの入力を委譲元のオブジェクトそのものとし、併せてそれへのaccessorを渡すことにより、計算結果の反映までをStrategyに委譲する方法は、1つの妥当な方法だと考えられる。
Strategyパターンの本質は、クラスの一部の処理を差し替え可能な形で別のクラス(Strategy)に委譲することにより、既存クラスを変更せずに処理を追加できるようにすることにあるので、Strategyクラスに対して既存クラスを隠蔽することは優先されないと、筆者は考える。

Strategyを呼び出すのはPhysicalObjectクラスのupdateVelocityメソッドで、このメソッドは存在する自分自身以外の全ての物体についてStrategyを呼び出すことにより、他の物体からの引力作用を自身の速度に反映させる。
物体の形状は、PhysicalObjectが、形状そのものではなく、形状の描画方法として定義する。抽象的なPhysicalObjectは形状を持たないので、具体的な描画方法はPhysicalObjectの各サブクラスが持つ。

ついでに、描画時の色をStrategyパターンで切り替えられるようにもしてみる。
物体の色は、物理的特徴の1つとしてPhysicalObjectに持ち、ColorStrategyによって標準色から変更されるとする。ColorStrategyがnullなら、標準色とする。

Strategyパターンの実装として、Strategyを無設定にすることが許されるかという問題があるが、よくわからないので、そういう問題があることをメモしておく目的で、ここでは無設定を許すとする。
おそらく、デフォルトの処理を本体に持つかStrategyに持つかという問題に関係していると思う。デフォルトの処理が原始的で変更される可能性が無く、本体にあって自然なら、Strategyとしてnullが許されるのではないだろうか。
また、Strategyが計算結果を返すなら、Strategyがnullな状態を許すと、それに対応する計算結果と同じ型の値が必要になるので、違和感があるような気がする。
GravityStrategyの方は、無重力とは引力作用が無いということではなく、ゼロの引力が働いているものとして扱うため、明示的にクラス化している。

以上の方針によって実装したのが、次のリンク先のページのJavaアプレットである。(ソースコードへのリンクもあり)
・StrategySample2のアプレットとソースコードのページ
フィールド上をクリックすると、何らかの物体が置かれる。
"Disc", "Wired Star", "Star"のいずれかのボタンを押すと、クリックして置かれる物体が切り替わる。
"Switch gravity strategy"を押すと、次に置かれる物体の引力のルールが切り替わる。
フィールドは、表示されている領域の3x3倍の大きさがあり、それより外に出た物体は削除される。
標準(デフォルト)の引力と色は次の通り。

無指定時の引力無指定時の色
Disc引力水色
Wired Star無重力マゼンタ
Star斥力黄色
あとはソースコード参照。

次のエントリーでは、大体同じものをVisitorパターンで作ってみる。
(続く)

今更ながら、かの著名な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で書いたコードを、下の方に貼っている。

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