Strategy パターンとは
定型的な処理の流れの中で、一部だけ、複数の処理のうちの1つを選んで処理することが必要な場合がある。その場合、どの処理を選ぶかは、引数で指定し、処理の中で分岐を行うだろう。
Fig1に示した例では、処理execute()は引数flgをとり、処理の途中でflgの値により、CompAまたはCompBのいずれかのfunc()を呼び出す、ということを行っている。ここでは、生成するオブジェクトの違いを示すために、CompAはオブジェクトの生成にcompFlagAというパラメータが必要で、CompBはオブジェクトの生成にcompFlagBというパラメータが必要であるとした。
class MyApp {
public void main(int flg,String compFlagA,String compFlagB) {
execute(flg,compFlagA,compFlagB);
}
public void execute(int flg,String compFlagA,String compFlagB) {
/*
* 前処理(データの準備など)
*/
Data data =getData();
/*
* dataに対して何らかの処理を行う
*/
if(flg == 1) {
CompA a = new CompA(compFlagA);
a.func(data);
} else if(flg == 2) {
CompB b = new CompB(compFlagB);
b.func(data);
}
/* 後処理 */
}
}
上記でCompAとCompBは、たとえば、ファイルへの出力と、メール送信のようにまったく無関係な処理であるかもしれない。よって、それぞれのオブジェクトの生成に必要なパラメータは異なる可能性がある。たとえば、ファイルへの出力の場合は、ファイル名、メール送信の場合は、メールアドレスや、SMTPサーバー名、など、さまざまであろう。ここでは、簡単に、それぞれcompFLagAとCompFLagBとしている。一方、メソッドの呼び出しパラメーターはどちらも処理のためのデータを渡しているはずなので、似たようなものになっているはずで、どちらも同じ引数dataを伴うfunc(data)を呼び出している。
ここで、CompA、CompBに同じメソッドを持たせる、つまり同じインターフェースを実装させることで、オブジェクトの生成処理の後の処理を統一化することを考える。
interface Comp {
public void func(Data data);
}
class CompA implements Comp {
public void func(Data data) {
:
}
:
}
class CompB implements Comp {
public void func(Data data) {
:
}
:
}
class MyApp {
public void main(int flg,String compFlagA,String compFlagB) {
execute(flg,compFlagA,compFLagB);
}
public void execute(int flg,String compFlagA,String compFlagB) {
/*
* 前処理(データの準備など)
*/
Data data =getData();
/*
* dataに対して何らかの処理を行う
*/
if(flg == 1) {
Comp c = new CompA(compFlagA);
} else if(flg == 2) {
Comp c = new CompB(compFlagB);
}
/* CompAとCompBはインターフェースCompを実装している */
c.func(data);
/* 後処理 */
}
}
Compは、void func(Data data)というシグネ(イ)チャーを持つインターフェイスである。
もし、関数内の分岐処理をFig2のようにできるのであれば、フラグで処理を分岐させるのではなく、処理そのものを引数に渡してしまえばいいのではないか。このように考えたのが、Strategy パターンである。
Strategy パターンの実装
「処理を渡す」というのは、インターフェースを渡すことで実現できるので、Fig2のMyAppのexecute()の引数をインターフェイスに変更してみる。
class MyApp {
public void main(int flg,String compFlagA,String compFlagB) {
if(flg == 1) {
Comp c = new CompA(compFlagA);
} else if(flg == 2) {
Comp c = new CompB(compFlagB);
}
execute(c);
}
public void main() {
public void execute(Comp c) {
/*
* 前処理(データの準備など)
*/
Data data =getData();
/*
* dataに対して何らかの処理を行う
*/
c.func(data);
/* 後処理 */
}
}
Fig2のMyAppとFig3のMyAppでは、CompAとCompBの生成処理が外に出た、ということだけある。しかしその意味はかなり大きい。
1つ目は、execute(Comp c)がCompA、CompB以外のオブジェクトも受け入れられるようになったということだ。たとえば、CompCを作成してexecute(Comp c)に渡すようになっても、execute(Comp c)の修正は行っていないので、execute(Comp c)のテストはしなくて良いのである。
2点目は、compFlagA,compFlagBのような、オブジェクトの生成のために必要なパラメータが、execute(Comp c)では不用になったということである。このことはオブジェクトの生成に柔軟性がでるということだ。しかも、それがexecute()とは無関係である、ということも重要な点である。従来は、柔軟性を得るために、execute()の修正が必要であったが、Strategy パターンにより、execute()が本来の処理だけ行うようになったのである。
「呼び出す処理を考える」から「呼び出される処理を考える」へ
Strategy パターンは、処理の内部の分岐を処理の外に押しやる効果がある。しかし分岐が移動しただけであり、分岐がなくなったわけではない。しかし、処理の深い場所で分岐をするのではなく、処理の前に分岐することが、処理関数のパラメータをシンプルにすると共に、柔軟性が出てくるのである。
また、内部で呼び出すコンポーネントの設計は、「呼び出される」という意識をもって行うようになるだろう。「呼び出す処理を考える」場合には、呼び出す関数(ここではfunc)の仕様をその場その場で考えて良いが、「呼び出される処理を考える」場合は、どのように呼び出されるのかがあらかじめ決められていて、パラメータが足りないからといって安易に増やすことは出来ない。となるとおのずから、汎用的な呼び出しになるよう設計に気をつけるようになるだろう。こういう効用もあるので、ぜひStrategy パターンを意識して実践してみると良い。
蛇足(Stateパターンとの比較)
Strategy パターンとState パターンは良く似ているといわれる。しかし両者はまったく使われる場面が違うように思う。
たとえば、パルスメータがあって、水道とガスの使用量を計測できるようなシステムの場合を今回の例に当てはめてみる。そもそもdataは、水道かガスのいずれかのデータを示している。よって呼び出されるコンポーネントは、そのデータにあわせて適切な方を呼び出す必要がある。コンポーネントは好きな形式に「切り替える」というより、データに依存して「切り替わる」、というような動作になるはずだ。このようなケースは、State パターンというだろう。この場合、MyAppを生成する段階、あるいは、内部の状態が変化する段階で、コンポーネントを登録することになるはずだ。
一方、Strategy パターンでは、呼び出し側が指定したコンポーネントを利用する。これはdataがどのように作成されたものか、関係ない。戦略と言われる由縁である。
参考文献
- オブジェクト指向における再利用のためのデザインパターン 改訂版 1999/11/1 初版
- リファクタリング:プログラミングの体質改善テクニック 2000/5/26 初版