|
注)以下の内容は「オブジェクト指向における再利用のためのデザインパターン 改訂版」(ソフトバンク パブリッシング 株式会社発行 著者:Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 監訳者:本位田 真一、吉田 和樹)からの引用を多分に含んでいます。 C++ でプログラミングをする上でのレベルアップには欠かせない名著です。 興味を持たれた方は是非ご購入をお勧めします。 |
|
Strategyパターン | ||||||
|
各アルゴリズムのカプセル化によりそれらを交換・拡張可能にする。 | ||||||
|
[動機] | ||||||
|
・クライアントが複数のアルゴリズムを保持すると、クライアントが大きくなり 複雑になる。 ・アルゴリズムは使い分けが必要で、複数のアルゴリズムを持つことが必要と される場合はすくなくない。 ・採用されているアルゴリズムが重要であればあるほど、新しいアルゴリズムの 追加や、アルゴリズムの変更が困難になる。 | ||||||
|
[適用可能性] | ||||||
|
・多くの似たクラスにおいて振る舞いのみが異なっている場合 そのクラスを複数作ることなく、アルゴリズムの差し替えで対応できる。 ・複数のアルゴリズムを必要とする場合 ・アルゴリズムがクライアントが知るべきでないデータを利用している場合 複雑でアルゴリズム特有なデータ構造を公開するのを避けることができる ・クライアントが多くの振る舞いを定義していて、これらがオペレーション内で 複数の条件文として現れている場合 条件分岐後の処理をStrategyクラスに移し換える | ||||||
|
[構成要素] | ||||||
|
Contextクラス …… Context:情勢・情況 ConcreteStrategyオブジェクトを備えている。 Strategyのオブジェクトに対する参照を保持する。 StrategyクラスがContextクラスのデータにアクセスする為の インターフェイスを備えても良い Strategyクラス …… Strategy:戦略 サポートする全てのアルゴリズムに共通のインターフェイスを宣言する。 ContextクラスはConcreteStrategyクラスにより定義されるアルゴリズムを 呼び出すためにこのインターフェイスを利用する ConcreteStrategyクラス(A/B/C/...) …… Concrete:具体的な Strategyクラスのインターフェイスを利用して、アルゴリズムを実装する | ||||||
|
[構造] | ||||||
| ||||||
|
[協調関係] | ||||||
|
・ContextとStrategy Contextオブジェクトはアルゴリズムに必要な全てのデータをStrategy オブジェクトに対して送る。 ContextオブジェクトはStrategyクラスのオペレーションの引数として 自身を渡すこともできる。 Contextオブジェクトはクライアントからの要求をStrategyオブジェクト に送る。クライアントはConcreteStragetyオブジェクトを生成し、これを Contextに渡す。その後クライアントはContextオブジェクトだけとやりとり をする。 | ||||||
|
[結果] | ||||||
|
長所 | ||||||
|
・アルゴリズムや振る舞いの集合がStrategyクラスの階層により定義されている ため、Contextオブジェクトで再利用が可能。 ・Strategyのサブクラスに別々にアルゴリズムをカプセル化することで Contextオブジェクトとは独立にアルゴリズムを変更することができる。 すなわち、「アルゴリズムの切り替え・拡張を容易にする」 ・異なる振る舞いを一つのクラスにまとめた場合、正しい振る舞いを選択するために 条件文を利用せざるを得ない。ところが、異なる振る舞いをStrategyの別々の クラスにカプセル化することにより、これらの条件文が排除される。 <Strategyパターンを使用しない場合> type = ENEMY_WEAKEST; switch(type) { case ENEMY_WEAKEST: ActEnemyWeakest(); break; case ENEMY_STRONGEST: ActEnemyWeakest(); break; } <Strategyパターンを使用した場合> enemy-> new Enemy( new StrategyEnemyWeakest ); //new StrategyEnemyStrongest; enemy->Action(); | ||||||
|
(欠点) | ||||||
|
・クライアントがConcreteStrategyパターンを選択する前に、それぞれの ConcreteStrategyパターンがどのように異なっているか知る必要がある。 また、新しいStrategyが追加されたときも、そのことをクライアントに 教える必要がある。 ・ConcreteStrategyの採用しているアルゴリズムによってはインターフェイスを 通して送られるすべての情報を利用しないことは十分にあり得る。 これは利用することのないパラメータをContextクラスが生成し、初期化する ことを意味している。もしこれが問題ならば、StrategyクラスとContextクラス の間の密な結合が必要になる。 ・ConcreteStrategyを複数生成することによりオブジェクト数が増加する。 もし複数のContextオブジェクトが共通するConcreteStrategyを利用する のであれば、それらを共有することでオブジェクト数の増加を抑えることが できる可能性がある。 もしStrategyが状態を必要とするのであれば、Contextオブジェクトに 情報を保持させ、ConcreteStrategyへ要求を出す際にコレを一緒に渡す。 (共有されるConcreateStrategyオブジェクトに状態を共有化できたとしても そのような状態を持たせるべきではない。Flyweightパターンより) | ||||||
|
[実装] | ||||||
|
・StrategyクラスとContextクラスのインターフェイス定義。 Contextオブジェクトのどのようなデータに対しても、Contextクラスの インターフェイスは、ConcreateStrategyオブジェクトが効果的にアクセス できるようにしなければならない。 コレを実現するには3つ方法が考えられる。 1. Strategyクラスにおける関数の引数を使ってContextクラスがデータを渡す。 という手法。 言い換えれば「ConcreteStrategyオブジェクトにデータを持っていく。」 これによりStrategyクラスとContextクラスを未結合の状態に保つことが できる。 しかし、Contextクラスが必要ないデータまで送る可能性がある。 2. Contextオブジェクトに引数として自身を送らせ、コレを受け取った ConcreteStrategyオブジェクトがそのContextオブジェクトに対して データを明示的に要求する。 という手法。 3. StrategyのオブジェクトにContextオブジェクトに対する参照を持たせて、 どのような情報の受け渡しも必要がないようにする。 という手法。 2 と 3 のどちらもStrategyオブジェクトは必要な情報だけを要求できる ようになる。 しかし、このためにはContextクラスはデータに対する複雑なインターフェイス を提供しなくてはならなくなり、これによりStrategyクラスとContextクラスは より密に結合することになる。 ・テンプレートパラメータとしてStrategyクラスを持たせる。 テンプレートパラメータとしてStrategyクラスを利用することで、Strategyクラスを Contextクラスに静的に結合することができ、これにより実行効率を上げることが できる。 ただし、コレは「Strategyクラスをコンパイル時に決定できる」「Strategyクラスを 実行時に変更する必要がない」ことが条件となる。 template <class AStrategy> class Enemy { void Action(){ theStrategy.DoAlgorithm(); //...}; //... private: AStrategy theStrategy; }; このようなクラスをインスタンス化する際に、パラメータとしてStrategyクラスが 与えられる。 class StrategyEnemyWeakest { public: void DoAlgorithm(); }; Enemy<StrategyEnemyWeakest> aEnemy; | ||||||
|
サンプルコード | ||||||
|
ここでは実際に私が開発している「ぐるぐる大回転」に使用しようとしているモノ をサンプルコードとして紹介する。 簡単にするために「ぐるぐる大回転」=「一般的な落ちモノ(ぷよぷよ)」と 考えていただいて構わない。 まず登場するクラスについて説明する。 MainStageクラス ・対戦が行われるメインステージ ・プレイヤーの個数だけFieldクラスを保持する ・ゲーム終了の判定。 ・キーボードからの入力を判定する。 ・各フィールドに対して計算・描画を指示。 Fieldクラス ・各プレイヤーに与えられるフィールド ・2人対戦の場合はFieldが二つ存在する ・ゲーム内の具体的な計算。 ・FallingGroupクラスの移動処理。 ・FallinObjectMapに登録されたFallingObjectに描画指示。 FallingObjectクラス ・落下してくる落ちモノのオブジェクト一つずつ ・コレを複数個備えたモノがFallingGroupクラス ・自身を描画する。 FallingGroupクラス ・FallingObjectインスタンスを複数持つ ・所持しているFallingObjectインスタンスへの描画指示。 ・回転・移動などの処理。 以上が今回サンプルとして紹介する私のプログラムの主要クラスである。 ココに今回は Enemyクラス ・Fieldクラスが持っているFallingGroupクラスのインスタンスを 操作する ・Strategyクラスのへのポインタをメンバ変数として持つ ・Strategyにより操作内容を決定する ・プレイヤーが操作しているフィールドには不要 ・プレイヤー VS プレイヤーの二人対戦の時は不要 StrategyEnemyクラス(抽象クラス) ・FallingGroupクラスをどのように動かすのか決定する StrategyEnemyRandomクラス(ConcreteStrategyクラス) ・全てを乱数により決定する StrategyEnemySimulateクラス(ConcreteStrategyクラス) ・全てをシミュレートし、最善の手段を選ぶ というクラスを組み込む。 プレイヤー数を管理しているのはMainStageクラスであるから Enemyクラスのインスタンスの生成の制御はMainStageクラスが 行う。 そこでPersonクラスという抽象クラスを作成し、そこからPlayerクラスと Enemyクラスを派生させる。 PersonクラスはFieldクラスのインスタンスを持ち、Playerクラスは キーボード入力を処理しFallingGroupを制御し、Enemyクラスは思考のための Strategyクラスをもち、StrategyEnemyクラスのサブクラスにより 行動を決定し、FallingGroupを制御することにする。 StageMainインスタンスはPersonクラスへのポインタを持ち、それを 通じて処理を行うため、実際にはどちらを制御しているのか知る必要は ない。 class Person // 抽象クラスとして生成する { public: virtual bool onMotion()=0; // 純粋仮想関数 // ... }; class Player :public Person // プレイヤー用 { public: virtual bool onMotion(); // ポリモルフィズム対応 // ... }; class Enemy // NPC用 { public: virtual bool onMotion(); // ポリモルフィズム対応 // ... }; bool Player::onMotion() // プレイヤーは入力受付 { GetInput(); // ... } bool Enemy::onMotion() // NPCは計算 { GetAction(); // ... } bool StageMain::onMotion() // StageMainはどちらか知る必要なし { for(num=0; num<PLAYER_MAX; ++) p_person[num]->onMotion(); // ... } EnemyStrategyにより行動を決定するクラスがEnemyクラスである。 class Enemy { public: Enemy(StrategyEnemy*); ~Enemy(){ delete m_strategy;}; virtual bool onMotion(); void GetAction(); // 行動決定 private: StrategyEnemy* m_strategy; FallingObject* FallingObjectMap[FIELD_WIDTH][FIELD_HEIGHT]; FallingGroup* mp_falling_group; ACT m_action[ACT_MAX]; }; 個々のConcreateStrategyの親となるはStrategyEnemyであり抽象クラス として実装する。 class StrategyEnemy // 抽象クラス { public: virtual void DecideAction( ACT& action, const FallingObject** const pFallingObjectMap, const FallingGroup* p_falling_group ) = 0; protected: StrategyEnemy(); }; 実際に行動を決定する動作は以下のようにポリモルフィズムを利用して 行われる。 void Enemy::GetAction() { m_strategy->DecideAction(m_action, pFallingObjectMap, mp_falling_group); } ここでStrategyEnemyのサブクラスを考える。StrategyEnemyRandomは行動の 全てを乱数によって決定する。 class StrategyEnemyRandom :public StrategyEnemy { public: StrategyEnemyRandom(); virtual void DecideAction( ACT& action, const FallingObject** const pFallingObjectMap, const FallingGroup* p_falling_group ); // ... }; StrategyEnemySimulationは行動を全てをシミュレートし決定する。 class StrategyEnemySimulation :public StrategyEnemy { public: StrategyEnemySimulation(); virtual void DecideAction( ACT& action, const FallingObject** const pFallingObjectMap, const FallingGroup* p_falling_group ); // ... }; StrategyEnemySimulationクラスは与えられた情報を有効に使うのに対し StrategyEnemyRandomクラスは与えられた情報を全て無視し、乱数により 行動を決定する。 Enemyクラスをインスタンス化する際に利用したいStrategyEnemyクラスの オブジェクトを渡すようにする。 Enemy* enemy_random = new Enemy( new StrategyEnemyRandom ); Enemy* enemy_simulation = new Enemy( new StrategyEnemyRandom ); StrategyEnemyクラスのインターフェイスは、そのサブクラスが実装するであろう 全てのアルゴリズムをサポートするために、注意深く設計する必要がある。 新しいアルゴリズムを導入するたびに、このインターフェイスを変更しなければ ならなくなるのは望ましくない。それは既存のサブクラスの変更を意味する。 一般に、StrategyクラスとContextクラスのインターフェイスにより、このパターンが どれだけ目標を達成できるかが決まる。 |