大幅な分岐なしで戦略パターンを実装できますか?


14

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>

しかし、これにより、データファイルを処理するための定型コードが追加されます。付与された、より簡単にユニットテスト可能で、比較的安定したコードですが、それでも複雑さが増します。

また、この種のコンストラクトは統合テストがうまくいきません。個々の戦略は今すぐテストする方が簡単かもしれませんが、追加するすべての新しい戦略はテストの複雑さを増すことになります。パターンを使用していなかった場合よりも少ないですが、それはまだあります。

この複雑さを緩和する戦略パターンを実装する方法はありますか?それとも、これはそれと同じくらい簡単で、さらに先へ進んでも、抽象化の別の層を追加するだけで、ほとんどまたはまったく利益がありませんか?


うーん... ...それはeval...のようなもので物事を簡素化することが可能かもしれません... Javaではなく、多分他の言語で動作するかもしれません?
FrustratedWithFormsDesigner

1
@FrustratedWithFormsDesignerリフレクションはJavaの魔法の言葉です
ラチェットフリーク

2
それでも、これらの条件はどこかで必要になります。それらをファクトリーにプッシュすることで、単にDRYに準拠していることになります。そうしないと、ifステートメントまたはswitchステートメントが複数の場所に現れる可能性があります。
ダニエルB

1
あなたが言及している脆弱性はしばしば呼ばれるオープン・クローズpricipleの違反
K3B

関連する質問で受け入れられた回答は、if-elseおよびswitchの代替として辞書/マップを示唆しています
-gnat

回答:


16

もちろん違います。IoCコンテナーを使用する場合でも、注入する具体的な実装を決定する条件がどこかにある必要があります。これが戦略パターンの性質です。

なぜこれが問題だと人々が考えるのか、私には本当にわかりません。Fowler's Refactoringのような一部の本には、他のコードの途中でスイッチ/ケースまたはif / elsのチェーンが表示された場合、その臭いを考慮して独自のメソッドに移動する必要があるというステートメントがあります。各ケース内のコードが2行以上、場合によっては2行である場合は、そのメソッドをファクトリメソッドにしてストラテジーを返すことを検討する必要があります。

一部の人々は、これをスイッチ/ケースが悪いことを意味すると考えています。これはそうではありません。ただし、可能であれば、それは単独で存在する必要があります。


1
悪いことでもないと思います。しかし、私は常にそれが維持するコードの量を減らす方法を探していて、それが質問を促しました。
マイケルK

Switchステートメント(および長いif / else-ifブロック)は不良であり、コードを保守可能な状態に保つために、可能な限り回避する必要があります。「すべての可能性がでている場合」があることを認めているとはいえ、いくつかのスイッチが存在しなければならないケースがあり、その場合には、単一の場所にそれを維持しようと、あまり雑用のそれを維持することになり1(それが簡単そうですコードを適切に分離しないと、同期を保つために必要な5つの場所のうち1つを誤って見逃してしまいます)。
シャドウマン

10

大幅な分岐なしで戦略パターンを実装できますか?

はい、すべてのStrategy実装が登録するハッシュマップ/辞書を使用します。ファクトリメソッドは次のようになります

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コンテナーを組み合わせたソリューションが適しています。


3
スイッチ/ケースを回避するために、戦略のハッシュマップ全体を手に入れましたか?の行よりもfactory.register(NEW_ORDER, NewOrder.class);クリーンな行の方が正確か、OCPの違反が少ないcase NEW_ORDER: return new NewOrder();か。
pdr

5
それはまったくきれいではありません。それは直感に反します。それは、オープンクローズドプリンシプルを改善するために、keep-it-simple-stupid-principleを犠牲にします。同じことは、制御の反転と依存関係の注入にも当てはまります。これらは、単純なソリューションよりも理解するのが難しいです。質問は、「重要な分岐のない実装...」ではなく、「より直感的なソリューションを作成する方法」ではありませんでした
k3b

1
分岐を回避したこともわかりません。構文を変更しました。
pdr

1
クラスが必要になるまでクラスをロードしない言語では、このソリューションに問題はありませんか?静的コードは、クラスがロードされるまで実行されません。他のクラスが参照しないため、クラスはロードされません。
ケビンクライン

2
個人的には、このメソッドが好きです。なぜなら、戦略を不変コードとして扱うのではなく、可変データとして扱うことができるからです。
タクロイ

2

「戦略」とは、少なくとも1回、少なくとも1回、代替アルゴリズムを選択することです。あなたのプログラムのどこかで誰かが決断を下さなければなりません-ユーザーかあなたのプログラムかもしれません。IoC、リフレクション、データファイルエバリュエーター、またはswitch / caseコンストラクトを使用してこれを実装する場合、状況は変わりません。


私はあなたのかつての声明に同意しません戦略は実行時に交換できます。たとえば、標準のProcessingStrategyを使用してリクエストの処理に失敗した後、VerboseProcessingStrategyを選択して処理を再実行できます。
dmux

@dmux:確かに、それに応じて私の答えを編集しました。
Doc Brown

1

戦略パターンは、考えられる動作を指定するときに使用され、起動時にハンドラーを指定するときに最適に使用されます。使用する戦略のインスタンスの指定は、Mediator、標準のIoCコンテナ、説明したようなFactoryを介して、または単にコンテキストに基づいて適切なものを使用して実行できます(多くの場合、戦略は幅広い用途の一部として提供されるためそれを含むクラスの)。

データに基づいてさまざまなメソッドを呼び出す必要があるこの種の動作については、手元の言語で提供されているポリモーフィズム構造を使用することをお勧めします。戦略パターンではありません。


1

私が働いている他の開発者と話をする中で、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();
}

(貧弱なエラー処理を無視してください-サンプルコード:))

ビルドがまだ必要なので、これは最も柔軟ではありません...しかし、それはコード変更を1行に減らします。データが工場コードから分離されているのが好きです。抽象クラス/インターフェースを使用して共通のメソッドを提供するか、コンパイル時の注釈を作成して特定のコンストラクター署名を強制することにより、パラメーターの設定などを行うこともできます。


これがホームページに戻る原因がわからないが、これは良い答えであり、Java 8ではリフレクションなしでコンストラクター参照を作成できるようになりました。
ジミージェームズ

1

各クラスに独自の順序タイプを定義させることにより、拡張性を改善できます。次に、工場は一致するものを選択します。

例えば:

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;
    }
}

1

許容されるブランチの数には制限があるはずです。

たとえば、switchステートメント内に8個以上のケースがある場合、コードを再評価し、リファクタリングできるものを探します。かなり頻繁に、特定のケースを別の工場にグループ化できることがわかりました。ここでの私の仮定は、戦略を構築する工場があるということです。

どちらにしても、これを避けることはできず、どこかで作業しているオブジェクトの状態またはタイプを確認する必要があります。次に、そのための戦略を構築します。古き良き関心事の分離。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.