Strategy
一言でいうと
Strategy (ストラテジー、 戦略) は、 振る舞いに関するデザインパターンの一つで、 アルゴリズムのファミリーを定義し、 それぞれのアルゴリズムを別個のクラスとし、 それらのオブジェクトを交換可能にします。
問題
ある日、 あなたはカジュアルな旅行者のためのナビゲーション・アプリを作成することにしました。 このアプリは、 ユーザーがどの都市でも素早くその地に慣れるのに役立つきれいな地図が特徴でした。
アプリの最も要望のあった機能の一つは、 自動経路計画でした。 ユーザーが住所を入力すれば、 地図上にその宛先への最速の経路を表示する、 というものです。
アプリの最初のバージョンでは、 道路上の経路しか作成できませんでした。 車で旅行する人々は喜んでいましたが、 休暇中に皆が運転したいわけではありません。 そこで、 次のバージョンでは、 徒歩の経路を作成するオプションを加えました。 そしてそのすぐ後に、 公共交通機関を使用した経路のオプションも追加しました。
しかし、 それは始まりに過ぎませんでした。 その後、 サイクリングのための経路作成を計画しました。 そしてさらに、 市内のすべての観光スポットを経由して経路を作成するための別のオプションも加えることにしました。
ビジネスの観点からは、 アプリは成功でしたが、 技術的には、 頭痛のタネとなりました。 新しい経路アルゴリズムを追加するたびに、 ナビゲーターの主要クラスのサイズは倍増しました。 ある時点で、 この野獣を飼うのに手を焼くようになりました。
バグ修正のためであれ、 街路スコアの微細な変更であれ、 アルゴリズムの一つを変更すると、 クラス全体にその影響が及び、 問題なく動いていたコードにエラーを引き起こす可能性が増大しました。
さらに、 チームワークは非効率的になりました。 成功したリリースの直後に採用されたチームメートは、 マージの競合を解決するのに時間を使い過ぎだと苦情を言います。 新しい機能を実装するには、 他の人が生成したコードと競合するため、 巨大なクラスを変更する必要が出てきます。
解決策
Strategy パターンに従うと、 あることを異なる様々な方法で行うクラスに対し、 これらのアルゴリズムの一つずつをストラテジー (戦略) と呼ばれる別々のクラスに抽出します。
コンテキストと呼ばれる元のクラスは、 数ある戦略のいずれか一つへの参照を格納するためのフィールドを持たなければなりません。 コンテキストは、 自分で作業を行う代わりに、 リンクされたストラテジー・オブジェクトに作業を委任します。
コンテキストは、 その仕事に適切なアルゴリズムを選択する責任を負いません。 代わりに、 クライアントは希望するストラテジーをコンテキストに渡します。 実際のところ、 コンテキストはストラテジーについての知識を持ちません。 コンテキストは、 すべてのストラテジーと、 同じ汎用インターフェースを通してやりとりします。 インターフェースは、 選ばれたストラテジーに内包されたアルゴリズムを使用するメソッド一つだけでできています。
こうすると、 コンテキストは具体的なストラテジーからは独立したものとなり、 コンテキストや他のストラテジーのコードを変更することなく、 新しいアルゴリズムを追加したり、 既存のものを変更できるようになります。
我々のナビゲーション・アプリでは、 それぞれのアルゴリズムを抽出して buildRoute
メソッド一つだけを持つ固有のクラスを作ります。 このメソッドは、 出発地と目的地を受け取り、 経路上の主要箇所のコレクションを返します。
同じ引数を与えても、 各経路計画クラスは違う経路を作り上げるかもしれません。 ナビの主クラスは、 どのアルゴリズムが選択されたかについては、 まったく感知しません。 なぜなら、 経由地を地図上に描画することがその一番の仕事だからです。 クライアントが UI 上のボタンなどにより経路選択の振る舞いを他のものと入れ替えれらるよう、 現在有効なストラテジーを切り替えるためのメソッドがクラスにはあります。
現実世界でのたとえ
空港に行くことを想像してみてください。 バスに乗ることもできまし、 タクシーを頼むこともできますし、 自転車でも行けます。 これらが、 輸送上の戦略です。 予算や時間の制限などの要因を勘案して、 どれを選ぶかを決めます。
構造
-
コンテキスト (Context) は、 複数の具象ストラテジーのいずれか一つへの参照を維持し、 ストラテジー・インターフェースのみを介してこのオブジェクトと通信します。
-
ストラテジー (Strategy) インターフェースは、 すべての具象ストラテジーに共通です。 コンテキストがストラテジーを実行するために使用するメソッドを宣言します。
-
具象ストラテジー (Concrete Strategy) は、 コンテキストが使用するアルゴリズムを種々の違う方法で実装します。
-
アルゴリズムの実行が必要となるたびに、 コンテキストはリンク先ストラテジー・オブジェクトの実行メソッドを呼びます。 コンテキストは、 どのような種類のストラテジーを相手に動作するのか、 どのようにアルゴリズムが実行されるかは認知しません。
-
クライアント (Client) は、 特定のストラテジーのオブジェクトを作成し、 コンテキストに渡します。 コンテキストは、 setter を公開しており、 クライアントは実行時にそれを使ってストラテジーを置き換えます。
擬似コード
この例では、 コンテキストは、 様々な算術演算を実行するために複数のストラテジーを使用します。
適応性
オブジェクト内でアルゴリズムのいろいろ異なる変種の使用を望み、 あるアルゴリズムから別のアルゴリズムに実行時に切り替えられるようにするには、 Strategy パターンを使用します。
Strategy パターンでは、 オブジェクトの振る舞いを実行時に間接的に切り替えることが可能です。 それは、 仕事の一部を異なる方法で実行するオブジェクトと結びつけることによって行います。
一部の振る舞いだけが異なり、 あとは同じようなクラスがたくさんある場合、 Strategy を使用します。
Strategy パターンを使用すると、 様々な振る舞いを個別のクラス階層に抽出し、 元のクラスを一つにまとめ、 重複したコードを削減できます。
クラスのビジネス・ロジックを、 ロジック全体からするとあまり重要ではないアルゴリズムの実装の詳細から分離します。
Strategy パターンを使用すると、 コード、 内部データ、 様々なアルゴリズムの依存関係をコードの残りの部分から分離できます。 クライアントはアルゴリズムを実行し、 実行時にそれらを切り替える単純なインターフェースが使えます。
クラスに、 同じアルゴリズムの変種を切り替えるための多数の条件文の塊が見受けられる場合、 このパターンを使用します。
Strategy パターンを適用することにより、 アルゴリズムを、 同一インターフェースを実装する個別のクラスに抽出し、 条件文を排除できます。 元のオブジェクトは、 アルゴリズムの多くの変種をその中で実装するのに代わり、 これらのオブジェクトの一つに実行を委任します。
実装方法
-
コンテキスト・クラス内で、 頻繁に変更されそうなアルゴリズムを見つけてください。 同じアルゴリズムの変種の切り替えを実行するための条件分の塊がその目印になるかもしれません。
-
アルゴリズムの変種間で共通となるストラテジー・インターフェースを宣言します。
-
アルゴリズムの変種全部を一つずつ個別のクラスに抽出します。 クラスはストラテジー・インターフェースを実装するようにします。
-
コンテキスト・クラス中に、 ストラテジー・オブジェクトへの参照を格納するためのフィールドを追加します。 そのフィールドの値を置き換えるための setter セッターを書き加えてください。 コンテキストはストラテジー・インターフェースを介してのみストラテジー・オブジェクトを使用する必要があります。 コンテキストは、 ストラテジーがデータにアクセスできるようにするためのインターフェースを定義してもかまいません。
-
コンテキストのクライアントは、 コンテキストの主な任務を実行するのに適切と思われるストラテジーとコンテキストを関連付ける必要があります。
長所と短所
- 実行時にオブジェクト内で使用されるアルゴリズムを交換可能。
- アルゴリズムの実装の詳細を、 それを使用するコードから分離可能。
- 継承を合成で置き換え可能。
- 開放閉鎖の原則。 コンテキストの変更の必要なしに新規ストラテジーを導入可能。
- ごく少数のアルゴリズムしかなく、 それらがほとんど変更されなければ、 このパターン導入で必要となる新規クラスやインターフェースでプログラムを必要以上に複雑化させる理由はない。
- クライアントは、 適切なストラテジーを選択するにあたり、 ストラテジー間の違いを認識する必要あり。
- 多くの近代的なプログラミング言語は関数型をサポートしており、 アルゴリズムの異種を匿名関数の集合中で実装可能。 余計なクラスやインターフェースでプログラムを膨らませることなく、 ストラテジー・オブジェクトとまったく同じようにこれらの関数が使用可能。
他のパターンとの関係
-
Bridge、 State、 Strategy (と限られた意味合いでは、 Adapter も) は、 非常に似た構造をしています。 実際のところ、 これらの全てのパターンは、 合成に基づいており、 仕事を他のオブジェクトに委任します。 しかしながら、 違う問題を解決します。 パターンは、 単にコードを特定の方法で構造化するためのレシピではありません。 パターンが解決する問題に関して、 開発者同士がするコミュニケーションの道具でもあります。
-
Command と Strategy は、 オブジェクトを何らかの操作でパラメーター化できるため、 似たように見えます。 しかし、 この二つはまったく異なる意図を持っています。
-
Command を使用して、 任意の操作をオブジェクトに変換できます。 操作のパラメーターは、 そのオブジェクトのフィールドになります。 変換により、 操作の実行を延期したり、 キューに入れたり、 コマンド履歴を保存したり、 遠隔サービスにコマンドを送信したりできます。
-
一方、 Strategy は通常、 同じことを行う異なる方法に関するものです。 単一のコンテキスト・クラス内でアルゴリズムを入れ替えることができます。
-
-
Template Method は、 継承に基づくもので、 サブクラスでその一部を拡張することによって、 アルゴリズムを部分的に変更できます。 Strategy は、 合成に基き、 オブジェクトの振る舞いを、 新しい振る舞いに関連した異なるストラテジーを与えることにより変更します。 Template Method は、 クラスのレベルで機能するので、 静的です。 Strategy はオブジェクトのレベルで機能するため、 実行時に動作を切り替えることができます。
-
State は、 Strategy の拡張と考えることができます。 どちらのパターンも合成 (コンポジション) に基づいており、 コンテキストの振る舞いの変更を、 ヘルパー・オブジェクトに仕事の一部を委任することにより行います。 Strategy では、 これらのオブジェクトは完全に独立しており、 互いを意識しません。 しかし、 State では、 具象状態間の依存関係を制限せず、 コンテキストの状態を自由に変更できます。