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パターンについては、本連載で回を改めて取り上げます。