StrategyパターンとVisitorパターンの応用例として、簡単な引力モデルのシミュレーションを作ってみる。引力の計算方法は外部クラスに存在させ、既存クラスを変更せずに追加/変更可能にする。
なるべく簡単にするため、次のような制限をつける。
・全物体の質量は同じ
・空間は2次元
・引力は2物体間の距離だけで決まる
まず、Strategyパターンを応用する例を考える。
引力の計算方法についてStrategyパターンを適用し、クラス構成は、次のようにする。
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パターンで作ってみる。
(続く)
相手側のクラスまたはStrategyによって引力の計算方法が変わる場合というのは、Strategyパターンで実現するのは難しい気がしたので、今回は省いた。
相手のクラスによって変わる場合、
(a)PhysicalObjectが相手のクラス毎にStrategyを持つ
(b)Strategy側でJavaでいうinstanceof()を使って処理を分ける
のどちらかになると思う。(b)の方が拡張しやすい気がするが、StrategyがPhysicalObjectのクラス階層を意識するのは何かが違う気がするし、スパゲッティコードになり易い気がするので、おそらく、面倒でもオブジェクト指向的には(a)が正しいのだろう。
相手のStrategyによって変わる場合は、さらに複雑だ。PhysicalObjectが相手のStrategy毎にStrategyを持つのはあり得ない(Strategyが増えると既存のクラスの修正が発生してしまう)ので、Strategy側でinstanceof()を使うなりして相手のStrategyのクラスを識別して、処理を分けることになると思う。
コメント