StrategyをEnumに持たせてはいけない理由
Strategyパターンで、どのようにStrategyをContextに与えるか。
メディアプレイヤーを実装する例を考える。
この場合は MediaPlayer が Context役になる。
//メディア再生クラス public class MediaPlayer { private MediaStrategy strategy; public void play() { strategy.play(); } } //プレイヤーで再生するための戦略インターフェース public interface IMediaStrategy { void play(); } //音楽を再生する場合の戦略 public class MusicStrategy implements IMediaStrategy { public void play() { //logic } } //動画を再生する場合の戦略 public class VideoStrategy implements IMediaStrategy { public void play() { //logic } }
この時、どのように MediaPlayer に Strategy を与えるか?
方法1
戦略をそのまま MediaPlayer に渡す。
public class MediaPlayer { private IMediaStrategy strategy; ... //戦略設定メソッド public void setStrategy(IMediaStrategy strategy) { this.strategy = strategy; } }
使い方
Player player = new Player(); player.setStrategy(new VideoStrategy());
メリット
- シンプル
- Strategyクラスを作成するだけでその戦略を利用可能
デメリット
- プログラマは戦略を既に知っている必要がある。
方法2
種別を列挙体に定義し、列挙体と戦略の関連付けを行うFactoryを作成する。
//種別を定義する列挙体 public enum MediaType { MUSIC, VIDEO; } //列挙体と戦略を関連付けするファクトリクラス public class MediaFactory { public static IMediaStrategy create(MediaType type) { switch(type) { case MUSIC: return new MusicStrategy(); case VIDEO: return new VideoStrategy(); } } } //プレイヤークラスは MediaType を受け取り、内部で Factory クラスを用いて戦略を得る。 public class MediaPlayer { private IMediaStrategy strategy; ... public void setType(MediaType type) { this.strategy = MediaFactory.create(type); } }
使い方
Player player = new Player();
player.setType( MediaType.MUSIC );
メリット
デメリット
- 新しい戦略クラスを追加した際にEnumの定義と Factory も更新する必要がある
方法3
DIコンテナを使う。こんな感じ?
public class MediaPlayer { private IMediaStrategy strategy; ... public void setType(MediaType type) { //外部に記述した設定ファイルなどを元に MediaType と Strategy を関連付け this.strategy = container.getStrategy(type); } }
使い方 方法2と一緒。
Player player = new Player();
player.setType( MediaType.MUSIC );
メリット
- 関連を外部に押し出すので自由度が高い
デメリット
- 既存のコードをDIコンテナを使用するものに移行するのは手間がかかる(多分)
方法4
Enum に Strategyを持たせる
public enum MediaType { MUSIC( new MusicStrategy() ), VIDEO( new VideoStrategy() ); //関連する戦略 private IMediaStrategy strategy; MediaType(IMediaStrategy strategy) { this.strategy = strategy; } public IMediaStrategy getStrategy() { return this.strategy; } } //プレイヤークラスは MediaType を受け取り、type が保持している strategy を利用する。 public class MediaPlayer { private IMediaStrategy strategy; ... public void setType(MediaType type) { this.strategy = type.strategy; } }
使い方 方法2と一緒。
Player player = new Player();
player.setType( MediaType.MUSIC );
メリット
- Enumと戦略がガッチリと組合うので、MediaType と Strategy の関連のバグが起こりえない。
- この場合では Strategy のインスタンスが常に一つしか生成されないので、一見シングルトン特性のようなものを持つ。FlyWight パターンのほうが近いかも。
デメリット
- ある特定の場合において設計を変えるほどの修正が必要になる。
その場合とは、Strategy が引数を受け取るような場合。
以下の例を考える。
Logを書き出すLoggerクラスがあり、書き出せるログの形式にテキスト形式、XML形式があるとする。
テキスト形式でログを書き出す戦略クラスを TextStrategy, 同様にXML形式の戦略クラスを XMLStrategy とする。
この時プログラマは TextStrategy, XMLStrategy にログを書き込む対象を渡すことができるべきである。
つまり、こう。
File file = new File("trace.log"); FileWriter filewriter = new FileWriter(file); ILoggerStrategy strategy = new TextStrategy(fileWriter);
このように、Enum に Strategy を持たせる構造は Strategy が引数を受け取る場合に対処が非常に面倒なことになる。
場合によっては以下のようなクソッタレなコードが生成されることもあるかもしれない。
LogType.TEXT.setWriter(fileWriter); logger.setStrategy(LogType.TEXT);
こんなコードが生成されないよう、Context と Strategy 間の関連は Enum + Factory によって生成されるべきである。
public static ILoggerStrategy create(LogType type, Writer writer) { switch(type) { case XML: return new XMLStrategy(writer); case TEXT: return new TextStrategy(writer); } }
あとは null や想定外の LogType が来たときの処理を加えればOK.
Strategy パターン使用時の注意点として、開発時にユーザーが戦略を追加できるべきかどうかによって設計が変わるという点である。自由に追加できるようにするなら Strategy をそのまま渡すのが一番シンプル。ただしドキュメントに既知の全ての Strategy を書くくらいの心遣いがあっても良い。
最後に、ここに載ってるコードは全てチェックも何もしてない。