Strategyパターンのおさらい
Strategyパターンは、ひとまとまりの計算をストラテジオブジェクトとして表すパターンです。Strategyパターンを使うことで、必要に応じて計算法を切り替えることが容易になります。典型的なStrategyパターンのプログラムには、図1に示すようなクラスおよびインタフェースが登場します。
それぞれのクラス、インタフェースの役割は次のとおりです。
- Strategy:計算を表すストラテジオブジェクトのインタフェース
- ConcreteStrategy:具体的な計算を表すストラテジオブジェクトのクラス
- Context:ストラテジによる計算を用いた処理を行うクラス
- クライアント:Contextにストラテジを登録するクラス[1]
[1] クライアントの役割は、GoF[1994](書籍『オブジェクト指向における再利用のためのデザインパターン』)には列挙されていませんが、本稿では説明を簡潔にするためにこれを採用します。
典型的なStrategyパターンのプログラムは、図2に示すようなシーケンスで動作します。
すなわち、Strategyパターンのプログラムはおおまかに次の2ステップで動作します。
-
ストラテジオブジェクトを
Contextに登録する -
Contextがストラテジオブジェクトによる計算を用いて処理を行う
Strategyパターンを用いている例には、Javaコレクションフレームワークの TreeSet クラスがあります[2]。TreeSet クラスは順序付き集合を表すコレクションクラスで、NavigableSet インタフェースを実装しています。要素の格納順を指定したいときには、TreeSet クラスのコンストラクタに Comparator インタフェースの引数を与えます。TreeSet クラスは、Comparator#compare メソッドを呼び出して要素間の大小関係を調べ、その結果に従って要素を格納します。
たとえば、従業員を表す Employee クラスのインスタンスを、TreeSet に名前順で格納するプログラムは、リスト1のように書けます。
import java.util.*;
/** 従業員. */
class Employee {
private final String name;
Employee(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
/** 従業員を名前で比較するストラテジ. */
class NameComparator implements Comparator<Employee> {
@Override
public int compare(Employee x, Employee y) {
return x.getName().compareTo(y.getName());
}
}
public class EmployeeStorer {
public static void main(String[] args) {
// 従業員を名前順で格納する集合
NavigableSet<Employee> employees = new TreeSet<>(new NameComparator());
employees.add(new Employee("Stephen"));
employees.add(new Employee("Aggi"));
employees.add(new Employee("Katrina"));
// 従業員を名前順で出力する
employees.forEach(e -> System.out.println(e.getName()));
// 出力: Aggi <改行> Katrina <改行> Stephen
}
}
TreeSet に関して、Strategyパターンに登場する役割は、それぞれ次のようなクラス・インタフェースによって担われています。
-
Strategy:
Comparatorインタフェース。 -
ConcreteStrategy:
Comparatorインタフェースを実装するクラス。【例】リスト1のNameComparatorクラス。 -
Context:
TreeSetクラス。 -
クライアント:
TreeSetクラスのインスタンスを作るクラス。【例】リスト1のEmployeeStorerクラス。
TreeSet クラスなどは、Strategyパターンを用いることで、次のような設計意図を実現しているものと考えられます。
- フレームワークが、ストラテジオブジェクトによって表される計算方法を知っている必要がない
- フレームワークを用いるプログラムが、新たな計算方法を定義できる
Template Methodパターンでも実現できる
実は、このような設計意図は、図3のようにクラスの継承を使ったTemplate Methodパターンでも実現できます。
Template Methodパターンにおいて、AbstractClass は、計算を行うメソッドを抽象メソッドとして提供し、その計算を用いる処理のメソッドを実装します。ConcreteClass は AbstractClass を継承し、具体的な計算を行うメソッドを実装します。
Template Methodパターンには、登場するインタフェース・クラスが少なくて済むという長所があります。一方でStrategyパターンには、計算を記述する ConcreteStrategy クラスを、計算を用いた処理を記述する Context クラスと継承関係にする必要がなく、完全に独立させられるという長所があります。このようにそれぞれに長所があるため、どちらを採用するべきかは場合によります。
TreeSet クラスと Comparator インタフェースの場合には、ConcreteStrategy が Context から独立していることの長所が活かせるため、Strategyパターンを採用していることが正解だといえるでしょう。
たとえば、リスト1の NameComparator クラスは、TreeSet クラスと継承関係になく、完全に独立しています。このため NameComparator クラスは、順序付きマップを表す TreeMap クラスや、リストをソートする Collections#sort メソッドなど、Comparator インタフェースのインスタンスをストラテジオブジェクトとして用いる他のクラスやメソッドと一緒に用いることができます。
Template Methodパターンについては、本連載で回を改めて取り上げます。

