Processingで学ぶ 実践的プログラミング専門課程

第25回デザインパターン(3) Strategy

導入

同じことをするにもいろんな方法があるものです。トンカツの添え物キャベツは千切りがよい人、角切りがよい人、手でちぎったのがよい人……。結局口に入って、消化され栄養を吸収された後は、繊維質が残って排出されます。どの方法でも結果は同じです。⁠トンカツとともにキャベツを食べる」という目的のための「キャベツの切断方法⁠⁠、それは好みと少々の意味合いによって、人それぞれに選択をします。

さて、今回は目的に応じたアルゴリズムの選択・切り替えを、柔軟に行えるコードを書くためのデザインパターンであるStrategyパターンを紹介します。GoF本(P.vii)でも初学者向けの題材であると勧められています。

展開

Strategy(ストラテジ)

プログラムは何らかの目的があって作られます。目的を果たすためにアルゴリズムをコードに落とします。そのアルゴリズムは1つとは限りません。アルゴリズムによって処理速度の違いや効率の善し悪しがあります。あるデータに対して高速だったアルゴリズムが、別のデータに対して低速であることは珍しくありません。ですから、データに応じてアルゴリズムを切り替えれば、実行速度の低下を防ぐことができるでしょう。

Strategyパターンの目的

Strategyという単語は、アルゴリズム(algorithm)という単語よりも広いニュアンスを持ちます。ここではアルゴリズムという単語の意味を「コンピュータに仕事を行わせる手順」とします。Strategyはコンピュータに限らず問題解決のための「戦略、戦術、方策」という意味があります。少々軍事的なニュアンスがありますが、アルゴリズムと改まって表現する程でもない手順や約束事も、広くとらえればStrategyです。

目的のために使用できるアルゴリズムが複数あるとします。場合に応じてアルゴリズムを切り替えて使用できれば便利です。アルゴリズムを部品化して、柔軟に切り替えて使用できる仕組みをまとめたのがStrategyパターンです。

また、アルゴリズムを表現するコードAを、それ利用する側のコードBから分離した場合、BからAのコードの細部は見える必要がない場合がほとんどです。必要なのは、Aが目的の出力をしてくれることです。AとBが一体となり、コードの細部があらわになっていると、コードが読みづらくなります。BからAのフィールドに対して不用意な操作を受けるかもしれません。これらを回避するためにも、Strategyパターンを適用してアルゴリズムをカプセル化する利点があります。

[Strategyパターン]
主となるコードからアルゴリズムを表現するコードを切り離し、部品化する。コードの実行時にアルゴリズムの切り替えを可能にする。

Strategyパターンのクラス図
画像

上図のStrategyパターンでは、Clientが計算結果を必要とし、Contextクラスに対して使用するアルゴリズム(ConcreteStrategy)を指定します。そして、ClientクラスはContextクラスのcontextMethodを呼んで必要な結果を得ます。

アルゴリズムを実装したConcreteStrategyクラスは複数あります。ConcreteStrategyクラスはStrategyインタフェイスを実装しています。

ContextクラスはStrategyインタフェイスを通してConcreteStrategyクラスのメソッドstrategyMethodを使用します。Contextクラスのコードでは、ConcreteStrategyクラスの実装がどのようなものかを知る必要がありません。Strategyインタフェイスを通じてメソッドstrategyMethodを呼べれば良いのです。

今後、異なるアルゴリズムを実装した新しいConcreteStrategyクラスを使い始めたならば、Clientが新しいクラスを使用することをContextクラスに対して指定するだけで、その他のコードについて変更が必要ない仕組みになっています。大変柔軟でシンプルな構造です。

Strategyパターンを使わないと

同じ目的のために作られた、異なるアルゴリズムを切り替えて使う場合、Strategyパターンを使わなければ、次のコードのように条件判断文で使用するメソッドを切り替えることになるでしょう。これは1からnまでの数の合計を取るコードです。異なるアルゴリズムを実装した2つのメソッドを交互に切り替えています。

同じ目的の異なるアルゴリズムを実装した複数のメソッドを、条件判断文で切り替えて使うMultiAlgorithm.pde
void setup(){
  int n = 10;
  println("Sum of 1 to " + n + " is ..."); 
  println("Switch algorithm.");
  for ( int i = 0; i < n; i++ ){
    int result = (i%2 == 0) ? sumSlow(n)
                            : sumFast(n);
    println(i + " : sum(" + n + ") = " +  result);
  }
}

int sumSlow(int n){
  println("<sumSlow>");
  int val = 0;
  for(int i=0; i <= n; i++){
    val = val + i;
  }
  return val;
}

int sumFast(int n){
  println("<sumFast>");
  int val = n * (n+1) / 2;
  return val;
}

アルゴリズムをクラスでカプセル化する

アルゴリズムを実装したメソッドを、クラスでカプセル化すると次のように書けます。このように書くメリットは、アルゴリズムをカプセル化したクラスを、部品として再利用できることです。

アルゴリズムをカプセル化したSlowSumFastSumクラスのコードは紙面の都合上省略しますので、リンク先のファイルで確認してください。

条件判断文で使用するクラスを切り替える部分のコード(コード全体はNoStrategyPattern.pdeを参照)
void setup(){
  int n = 10;
  println("Sum of 1 to " + n + " is ..."); 
  println("Switch algorithm.");
  SlowSum ss = new SlowSum();
  FastSum fs = new FastSum();
  for ( int i = 0; i < n; i++ ){
    int result = (i%2 == 0) ? ss.sum(n)
                            : fs.sum(n);
    println(i + " : sum(" + n + ") = " +  result);
  }
}

Stragegyパターンを使う

それでは、Strategyパターンを具体的に利用してみます。

Strategyパターンを使って書き換えたWithStrategyPattern.pdeの一部)
void setup(){
  int n = 10;
  println("Sum of 1 to " + n + " is ..."); 
  println("Switch algorithm.");
  Strategy st;
  for ( int i = 0; i < n; i++ ){
    st = (i%2 == 0) ? new ConcreteStrategy1() 
                    : new ConcreteStrategy2(); 
    Context c = new Context(st);
    int result = c.getSum(n);
    println(i + " : sum(" + n + ") = " +  result);
  }
}

sketchの全体を以下に示します。

このsketchでは、ConcreteStrategyクラスのオブジェクトを条件判断文で切り替えています。Strategyパターンを使わない場合と全く変わらないように見えますか? それどころかクラスが増えてややこしくなったように感じるのではないでしょうか。確かにその感覚は正しいと思います。この問題を解くために、ここまでデザインパターンを適用する必要はないでしょう。しかし、注意深くコードを分析してください。条件判断文で切り替えているのは「計算アルゴリズムを実装しているオブジェクトへの参照」です。合計を計算する仕事を請け負うContextクラスは、使用するConcreteStrategyクラスが違っても共通です。

合計を計算する仕事を請け負うContextクラスのコードContext.pde
class Context{
  Strategy currentStrategy;
  Context(Strategy s){
    currentStrategy = s;
  }
  public int getSum(int n){
    return currentStrategy.sum(n);
  }
}

Contextクラスから具体的なアルゴリズムを記述したコードが完全に切り離されています。具体的なアルゴリズムを記述したコードを持つオブジェクトへの参照は、StrategyインタフェイスのオブジェクトcurrentStrategyが持つだけです。ConcreteStrategyにあたるクラスがどんな実装を持つかは、Contextクラスには関係なくなりました。主となるsketchでは次のように用いられています。

主となるsketch、WithStrategyPattern.pdeの一部
    st = (i%2 == 0) ? new ConcreteStrategy1() 
                    : new ConcreteStrategy2(); 
    Context c = new Context(st);
    int result = c.getSum(n);

Contextクラスは生成時にコンストラクタへアルゴリズムを担当するオブジェクトへの参照を受け取ります。その後は、どんなアルゴリズムを実装したオブジェクトを受け取ったとしても、全く同じメソッドgetSumを呼べば目的の仕事が果たせます。

ここでは、合計を求めるという仕事をする際に、

  • 「合計を求めるクラス」を切り替えて使うのか
  • 合計を求めるアルゴリズムを実装したメソッドを持つクラスを切り替えて、⁠合計を求めるクラス」はそのまま使うのか

しかしていません。そのため、それぞれのクラスがコンパクトで、Strategyパターンを使用するメリットが強く浮き出ませんでした。

Strategyパターンを使用するメリットを考えるため、次の実例を紹介します。

事例:Robocodeでロボットの動作アルゴリズムを動的に切り替える

これは実際に私がRobocodeに取り組む中で体験したことです。

移動する相手ロボットに、私のロボットが発射した弾を当てるためには、相手ロボットの動きに応じた「移動先予測アルゴリズム」が必要です。また、相手の攻撃を避け、相手を攻撃するために有利な位置取りをする必要もあります。そのため、相手ロボットに対して次のような事柄を含めて様々なことを検討して、それぞれに対応したコードをロボットに組み込みます。

  • 直線的に動く相手なのか、曲線的に動く相手なのか。
  • 直線的に動くとしても、前後に往復するのか、何かのタイミングで方向転換を行うのか。
  • 移動速度の変化にパターンはないか。

これらは単純に1つ、2つの式を計算して得られる結果ではありません。計算に必要なデータは多く、またリアルタイムに変化しますから、アルゴリズムを記述したコードは大きくなります。

主となるコードに複数のアルゴリズムを記述したコードが含まれると、コードの総量は大変なものになります。そこでStrategyパターンを適用すれば、攻撃や移動のためのコードが部品化され、主となるコードから切り離されます。主となるコードが劇的にシンプルになります。シンプルなコードはメンテナンスがしやすく、パターンを導入することで負ったコードの複雑さを補って余りあります。そして、シンプルなコードは高速な処理を期待できます。

Strategyパターンを適用せずに、アルゴリズムを切り替えて動くロボットのコードを書ことを考えると、ぞっとします。

演習

演習1(難易度:middle)

ディスプレイウインドウにアルゴリズム切り替えのための白いボタンを表示してください。そして、白いボタンをクリックしたときに、マウスポインタのX, Y座標値の最大公約数を求めるアルゴリズムを切り替えてください。その際、Strategyパターンを使って書き換えてください。また、アルゴリズムを切り替えたら、切り替えたことが分かる文字列をコンソールに表示しましょう。

まとめ

  • Strategyパターンを使うメリットを学びました。
  • Strategyパターンの使い方を学びました。

学習の確認

それぞれの項目で、Aを選択できなければ、本文や演習にもう一度取り組みましょう。

  1. Strategyパターンを使うメリットがわかりましたか?
    1. メリットが分かった。自分のプログラミングにも使いたい。
    2. 本文に解説されたメリットを理解することはできたが、それほどメリットだとは感じない。
    3. 本文が理解できない。
  2. Strategyパターンを使えるようになりましたか?
    1. 使えるようになった。自分のプログラミングにも活用できそうだ。
    2. 本文の例を理解することはできたが、自分のプログラミングに活用できる気がしない。
    3. 本文の例が理解できない。

参考文献

  • 『増補改訂版Java言語で学ぶデザインパターン入門』⁠結城浩 著、ソフトバンククリエイティブ
    • 誰もが認める最も分かりやすいデザインパターン入門書。
  • 『オブジェクト指向における再利用のためのデザインパターン』⁠Eric Gamma 著、ソフトバンククリエイティブ
    • デザインパターンの原典。別称『GoF本⁠⁠。
  • 『Java デザインパターン徹底攻略』⁠日立ソフトウェアエンジニアリング⁠株⁠インターネットビジネス部 著、技術評論社
    • 前掲のGoF本の解説書で、サンプルはJava言語。絶版。強く再版を望みます。
  • 『Robocodeで学ぶ 一歩先のJavaプログラミング』⁠平田敦 著、カットシステム
    • Robocodeを使ったJava言語の学習書です。敵ロボットに照準を合わせるためのアルゴリズムにStrategyパターンを適用した解説をしています。参考にしてみてください。

演習解答

  1. Strategyパターンを適用したsketchを以下に示します。これらのpdeファイルをすべてGCD_Strategyフォルダに納めてください。実行時にディスプレイウインドウ上の白いボタンをクリックすると、アルゴリズムが切り替わります。座標値の最大公約数や使用したアルゴリズムがコンソールに刻々と表示される様子を確認してください。

おすすめ記事

記事・ニュース一覧