Java : Strategy パターン (図解/デザインパターン)
Strategy パターンとは、GoF によって定義されたデザインパターンの1つです。
プログラムの実行時に、処理を動的に切り替えるための設計です。
本記事では、Strategy パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Strategy パターンとは、
- ある1つのクラスがあり
- そのクラスの変更なしで、処理の一部を動的に切り替える
という目的のための設計です。
Strategy は、日本語的に発音すると「ストラテジー」となります。
意味は「戦略」ですね。
【 Strategy パターンのイメージ図 】
ストラテジーA、ストラテジーB、ストラテジーC ... と追加していことで、いくらでも処理を拡張できるのがメリットです。
しかも、ストラテジーを使う側のクラスは修正が必要ありません。
いわゆる 開放閉鎖の原則
- ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。
ですね。
オブジェクト指向の ポリモーフィズム(多態性) を活用していれば、自然と Strategy パターン になることも多いのではないでしょうか。
それだけ、汎用性があって有用なパターンだと思います。
また、Strategy パターンは、これだ!という決まった形にはこだわらなくてもよいでしょう。
例えば、あるクラスにストラテジーを設定するのは、コンストラクタでもよいし、適当な Setter メソッドでも OK です。
大事なのは、処理(アルゴリズム) を動的に切り替えることができる、という点です。
関連記事:
クラス図とシーケンス図
上の図は、Strategy パターンの一般的なクラス図です。
このクラス図をそのままコードにしてみましょう。
public class Context {
private final Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void execute() {
System.out.println("-- Context.execute --");
strategy.execute();
}
}
public interface Strategy {
void execute();
}
public class ConcreteStrategyA implements Strategy {
@Override
public void execute() {
System.out.println("StrategyA!");
}
}
public class ConcreteStrategyB implements Strategy {
@Override
public void execute() {
System.out.println("StrategyB!");
}
}
final var context1 = new Context(new ConcreteStrategyA());
context1.execute();
// 結果
// ↓
//-- Context.execute --
//StrategyA!
final var context2 = new Context(new ConcreteStrategyB());
context2.execute();
// 結果
// ↓
//-- Context.execute --
//StrategyB!
context1 と context2 は同じ Context クラスを使っていますが、Strategy を変えることにより処理の内容が変わりました。
これが Strategy パターンの基本となります。
コード例
もう少し具体的な例も見てみましょう。
1行ずつ文字列を追加して、それをコンソールに出力する Printer クラスを考えてみます。
public class Printer {
public void add(String line) {
...
}
public void print() {
...
}
}
Printer クラスは、
- add メソッド : 1行の文字列を追加
- print メソッド : 追加した文字列をコンソールに出力
という機能を持ちます。
Strategy パターンを使わないケース
Printer クラスは、当初、
- プレーンなテキストで出力
という機能のみが必要でした。
チームリーダー「とりあえずプレーンなテキスト出力だけでよいので、実装お願いします」
Aさん「了解しました!」
それならと、Aさんは次のように実装しました。
public class Printer {
private final List<String> lines = new ArrayList<>();
public void add(String line) {
lines.add(line);
}
public void print() {
for (final var line : lines) {
System.out.println(line);
}
}
}
final var printer = new Printer();
printer.add("りんご");
printer.add("バナナ");
printer.add("みかん");
printer.print();
// 結果
// ↓
//りんご
//バナナ
//みかん
問題なく機能していますね。
しかし、しばらくてチームリーダーから次のような依頼が…
チームリーダー「Printer クラスに、HTML形式で出力する機能も追加してください」
Aさん「了解しました」
そこでAさんは次のようなコードを実装しました。
public class Printer {
public enum Type {
TEXT,
HTML,
}
private final Type type;
private final List<String> lines = new ArrayList<>();
public Printer(Type type) {
this.type = type;
}
public void add(String line) {
lines.add(line);
}
public void print() {
switch (type) {
case TEXT -> {
for (final var line : lines) {
System.out.println(line);
}
}
case HTML -> {
final var str = String.join(System.lineSeparator() + " ", lines);
System.out.printf("""
<html>
<body>
%s
</body>
</html>
""", str);
}
}
}
}
結果は次のようになりました。
【プレーンテキスト出力版】
final var printer = new Printer(Printer.Type.TEXT);
printer.add("りんご");
printer.add("バナナ");
printer.add("みかん");
printer.print();
// 結果
// ↓
//りんご
//バナナ
//みかん
【HTML出力版】
final var printer = new Printer(Printer.Type.HTML);
printer.add("りんご");
printer.add("バナナ");
printer.add("みかん");
printer.print();
// 結果
// ↓
//<html>
// <body>
// りんご
// バナナ
// みかん
// </body>
//</html>
問題なく動作しているようです。
出力する形式を 列挙型(enum) で定義して、
public enum Type {
TEXT,
HTML,
}
それを print メソッド内の switch式 で分岐させています。
public void print() {
switch (type) {
case TEXT -> {
...
}
case HTML -> {
...
}
}
}
さて、しばらくして、さらに追加の依頼がきました。
チームリーダー「JSON形式とバイナリデータ形式も追加お願いします」
Aさん「……」
このように要件が追加されるのは、プログラマあるあるです。
最初からすべてをきっちり決めることは難しいです。チームリーダーを責めるべきではないでしょう。
Aさんは次のように Printer クラスを修正しました。
public enum Type {
TEXT,
HTML,
JSON,
BINARY,
}
public void print() {
switch (type) {
case TEXT -> {
...
}
case HTML -> {
...
}
case JSON -> {
...
}
case BINARY -> {
...
}
}
}
しかし、こんな調子で機能を追加していったら、Printer クラスがどんどん 肥大化 してしまいます。
かなりシンプルな Printer クラスでこれですから、実際の開発ではもっと複雑で修正しづらい状況になってしまうでしょう。
これは 開放閉鎖の原則
- ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。
に反しています。
この Priner クラスは 修正に対して閉じていない ということですね。
Strategy パターンを使うケース
次は、Strategy パターン を使う例を見てみましょう。
コンソールへ出力する処理を、Strategy として分離します。
まずは Strategy インタフェースとその実装です。
public interface Strategy {
void print(List<String> lines);
}
public class TextStrategy implements Strategy {
@Override
public void print(List<String> lines) {
for (final var line : lines) {
System.out.println(line);
}
}
}
public class HtmlStrategy implements Strategy {
@Override
public void print(List<String> lines) {
final var str = String.join(System.lineSeparator() + " ", lines);
System.out.printf("""
<html>
<body>
%s
</body>
</html>
""", str);
}
}
Strategy インタフェースでは、先ほど switch式 で分岐していた処理を、それぞれ 別のクラス として実装します。
- TextStrategy : プレーンなテキストとして出力
- HtmlStrategy : HTML形式で出力
ですね。
public class Printer {
private final Strategy strategy;
private final List<String> lines = new ArrayList<>();
public Printer(Strategy strategy) {
this.strategy = strategy;
}
public void add(String line) {
lines.add(line);
}
public void print() {
strategy.print(lines);
}
}
Printer クラスでは、列挙型(enum) で条件分岐する代わりに、Strategy へ処理を 委譲 します。
【プレーンテキスト出力版】
final var printer = new Printer(new TextStrategy());
printer.add("りんご");
printer.add("バナナ");
printer.add("みかん");
printer.print();
// 結果
// ↓
//りんご
//バナナ
//みかん
【HTML出力版】
final var printer = new Printer(new HtmlStrategy());
printer.add("りんご");
printer.add("バナナ");
printer.add("みかん");
printer.print();
// 結果
// ↓
//<html>
// <body>
// りんご
// バナナ
// みかん
// </body>
//</html>
結果も問題なしです。
Strategy パターンを使うと、Printer クラスの 修正なし で、どんどん機能が拡張できるのが分かりますでしょうか。
機能を拡張するたびに、~Strategy クラスは増えていきます。(JsonStrategy, BinaryStrategy など)
しかし、それぞれのクラスはそんなに巨大にはならないでしょう。
拡張のたびに、特定のクラス (Printer クラスなど) だけが際限なく巨大になっていくよりかは全然マシです。
- ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。
になっていますね。(何度も引き合いに出してすみません…)
Strategy パターンを使った Printer クラスは、
- Strategy による機能の拡張が可能 (開いている)
- Priner クラス自体の修正は必要なし (閉じている)
となります。
Java 標準API で使われている例
Java 標準API で Strategy パターンが使われている例を見てみましょう。
Java のロギングAPI は、Java でログを出力するための API です。
ただ、少し使い方が難しいので、ここではなんとなく雰囲気を感じていただければ大丈夫です。
関連 : ロギング(ログ出力)の基本
ロギングAPI では、ログの出力形式を Formatter インタフェースを使って切り替えることができます。
この Formatter が、Strategy にあたります。
例えば、XML形式で出力したり、シンプルなテキスト形式で出力できます。
Handler はログの出力先を表します。
例えばコンソールへ出力する ConsoleHandler や、ファイルへ出力する FileHandler があります。
final var logger = Logger.getLogger("com.example.logging");
logger.setUseParentHandlers(false);
final var handler = new ConsoleHandler();
logger.addHandler(handler);
// XMLFormatter を使います。(★この部分がストラテジーにあたります)
handler.setFormatter(new XMLFormatter());
// ログ出力
logger.info("テスト!");
// 結果
// ↓
//<?xml version="1.0" encoding="UTF-8" standalone="no"?>
//<!DOCTYPE log SYSTEM "logger.dtd">
//<log>
//<record>
// ... 省略 ...
// <message>テスト!</message>
//</record>
// SimpleFormatter に切り替えます。(★この部分がストラテジーにあたります)
handler.setFormatter(new SimpleFormatter());
// ログ出力
logger.info("テスト!");
// 結果
// ↓
//2月 23, 2100 8:17:28 午後 xxxx method
//情報: テスト!
Handler.setFormatter メソッドで
を設定することにより、簡単にログの出力形式を切り替えています。
なんなら、自分で MyFormatter を作って、独自の出力形式にすることもできます。
Strategy パターンを使うことで、ログの出力形式の拡張がやりやすくなっているのが分かりますね。
まとめ
Strategy パターンとは、
- ある1つのクラスがあり
- そのクラスの変更なしで、処理の一部を動的に切り替える
という目的のための設計です。
切り替える処理の部分は、ストラテジーA、ストラテジーB、ストラテジーC ... と追加していけるので、拡張しやすいのがメリットです。
しかも、ストラテジーを使う側のクラスは修正が必要ありません。
汎用性があってとても有用なパターンだと思います。
ぜひ有効に活用していきたいですね。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Template Method パターン
- Visitor パターン