Strategyパターンは、巨大なif ... else構成を回避し、機能の追加または置き換えを容易にするためにうまく機能します。しかし、それでも私の意見には1つの欠陥が残っています。どの実装でも、まだ分岐構造が必要なようです。ファクトリまたはデータファイルの可能性があります。例として、注文システムを取り上げます。
工場:
// All of these classes implement OrderStrategy
switch (orderType) {
case NEW_ORDER: return new NewOrder();
case CANCELLATION: return new Cancellation();
case RETURN: return new Return();
}
この後のコードは心配する必要はありません。新しい注文タイプを追加できる場所は1か所だけですが、このコードのセクションはまだ拡張できません。それをデータファイルに引き出すと、読みやすさがある程度向上します(議論の余地があると思います)。
<strategies>
<order type="NEW_ORDER">com.company.NewOrder</order>
<order type="CANCELLATION">com.company.Cancellation</order>
<order type="RETURN">com.company.Return</order>
</strategies>
しかし、これにより、データファイルを処理するための定型コードが追加されます。許可された、ユニットテストが容易で比較的安定したコードですが、それでもなお複雑さが増します。
また、この種の構成は統合テストがうまくいきません。個々の戦略は今やテストする方が簡単かもしれませんが、追加する新しい戦略はすべて、テストがさらに複雑になります。 しなかったパターンを使用した場合よりも少なくなりますが、それでも存在します。
この複雑さを軽減する戦略パターンを実装する方法はありますか?それとも、これはできるだけ簡単で、さらに進んでみても、抽象化の別のレイヤーが追加されるだけで、ほとんどまたはまったくメリットがありませんか?
もちろん違います。 IoCコンテナーを使用する場合でも、注入する具体的な実装を決定する条件がどこかにある必要があります。これが戦略パターンの性質です。
なぜこれが問題であると人々が考えるのか、私にはよくわかりません。 Fowler's Refactoring のような一部の本には、他のコードの真ん中にスイッチ/ケースまたはif/elsesのチェーンが表示された場合、そのにおいを考慮して移動する必要があるという記述があります独自のメソッドにそれを。各ケース内のコードが2行以上の場合は、そのメソッドをファクトリメソッドにして、戦略を返すことを検討する必要があります。
一部の人々はこれをスイッチ/ケースが悪いことを意味すると考えています。これはそうではありません。ただし、可能であれば、それ自体で存在する必要があります。
戦略パターンは、大きな分岐なしで実装できますか?
はい、すべての戦略の実装が登録されているハッシュマップ/辞書を使用します。ファクトリーメソッドは次のようになります
Class strategyType = allStrategies[orderType];
return runtime.create(strategyType);
すべての戦略の実装は、ファクトリをそのorderTypeとクラスの作成方法に関するいくつかの情報とともに登録する必要があります。
factory.register(NEW_ORDER, NewOrder.class);
言語でサポートされている場合は、登録に静的コンストラクターを使用できます。
Registerメソッドは、ハッシュマップに新しい値を追加する以外に何もしません。
void register(OrderType orderType, Class class)
{
allStrategies[orderType] = class;
}
[更新2012-05-04]
このソリューションは、ほとんどの場合私が好む元の「スイッチソリューション」よりもはるかに複雑です。
ただし、戦略が頻繁に変更される環境(つまり、顧客、時間などに応じた価格計算)では、IoC-Containerと組み合わせたこのハッシュマップソリューションが優れたソリューションになる可能性があります。
「戦略」とは、少なくともonceではなく、代替アルゴリズムから選択することです。あなたのプログラムのどこかに誰かが決定を下さなければならない-多分ユーザーかあなたのプログラム。 IoC、リフレクション、データファイルエバリュエーター、またはスイッチ/ケースコンストラクトを使用してこれを実装しても、状況は変わりません。
私が働いている他の開発者と話していると、Javaに固有の別の興味深い解決策が見つかりましたが、そのアイデアは他の言語でも機能すると確信しています。クラス参照を使用して列挙型(またはマップ、あまり重要ではない)を作成し、リフレクションを使用してインスタンス化します。
enum FactoryType {
Type1(Type1.class),
Type2(Type2.class);
private Class<? extends Type> clazz;
private FactoryType(Class<? extends Type> clazz) {
this.clazz = clazz;
}
public Class<? extends Type> getTypeClass() {
return clazz;
}
}
これにより、出荷時のコードが大幅に削減されます。
public Type create(FactoryType type) throws Exception {
return type.getTypeClass().newInstance();
}
(悪いエラー処理は無視してください-サンプルコード:))
mostフレキシブルではありません。ビルドがまだ必要なためです...しかし、コードの変更を1行に減らします。データがファクトリーコードから分離されているのが好きです。抽象クラス/インターフェースを使用して共通のメソッドを提供したり、コンパイル時のアノテーションを作成して特定のコンストラクターシグネチャを強制したりすることで、パラメーターの設定などを行うこともできます。
戦略パターンは、可能な動作を指定するときに使用され、起動時にハンドラーを指定するときに最適に使用されます。使用する戦略のインスタンスの指定は、メディエーター、標準のIoCコンテナー、記述したようなFactoryを介して、または単にコンテキストに基づいて適切なものを使用することによって実行できます(多くの場合、戦略は幅広い用途の一部として提供されるため)それを含むクラスの)。
データに基づいてさまざまなメソッドを呼び出す必要があるこの種の動作については、手元の言語によって提供される多態性コンストラクトを使用することをお勧めします。戦略パターンではありません。
許容できるブランチの数には制限があるはずだと思います。
たとえば、switchステートメント内に8つを超えるケースがある場合、コードを再評価して、リファクタリングできるものを探します。多くの場合、特定のケースを別のファクトリにグループ化できることがわかります。ここでの私の仮定は、戦略を構築する工場があるということです。
どちらにしても、これを回避することはできず、どこかで、操作しているオブジェクトの状態またはタイプを確認する必要があります。次に、そのための戦略を構築します。古き良き懸念の分離。
各クラスに独自の注文タイプを定義させることにより、拡張性を向上させることができます。次に、一致するものを工場が選択します。
例えば:
public interface IOrderStrategy
{
OrderType OrderType { get; }
}
public class NewOrder : IOrderStrategy
{
public OrderType OrderType { get; } = OrderType.NewOrder;
}
public class OrderFactory
{
private IEnumerable<IOrderStrategy> _strategies;
public OrderFactory(IEnumerable<IOrderStrategy> strategies) // Injected by IoC container
{
_strategies = strategies;
}
public IOrderStrategy Create(OrderType orderType)
{
IOrderStrategy strategy = _strategies.FirstOrDefault(s => s.OrderType == orderType);
if (strategy == null)
throw new ArgumentException("Invalid order type.", nameof(orderType));
return strategy;
}
}