最後の 2 つのレッスンでは、テンプレート パターンについて学びました。テンプレート モードは主にコードの再利用と拡張の役割を果たします。さらに、テンプレート モードに似ていますが、より柔軟に使用できるコールバックについても説明しました。これらの主な違いはコードの実装であり、テンプレート パターンは継承に基づいて実装され、コールバックは合成に基づいて実装されます。
今日、私たちは別の行動パターンである戦略パターンを学び始めます。実際のプロジェクト開発でもこのモードがよく使われます。最も一般的なアプリケーション シナリオは、長時間にわたる if-else または switch 分岐の判断を回避するために使用することです。ただし、それ以上の効果があります。テンプレート モードと同様に、フレームワーク拡張ポイントなどを提供することもできます。
戦略パターンについては2回に分けて解説していきます。今回は戦略パターンの原理と実装、そして分岐判定ロジックを回避するための活用方法について解説します。次のレッスンでは、具体的な例を使用して、戦略パターンの適用シナリオと実際の設計意図を詳しく説明します。
それでは早速、今日の学習を正式に始めましょう!
戦略パターンの原則と実行
ストラテジーモード、英語の正式名は Strategy Design Pattern です。GoF の書籍「デザイン パターン」では、次のように定義されています。
アルゴリズムのファミリーを定義し、それぞれをカプセル化して、交換可能にします。戦略では、アルゴリズムを使用するクライアントとは独立してアルゴリズムを変更できます。
中国語に翻訳すると、アルゴリズム クラスのファミリーを定義し、各アルゴリズムを個別にカプセル化して、相互に置換できるようにします。Strategy パターンは、アルゴリズムを使用するクライアントとは独立してアルゴリズムを変更できます (ここでのクライアントとは、アルゴリズムを使用するコードを指します)。
ファクトリ パターンはオブジェクトの作成と使用を分離するものであり、オブザーバー パターンは観察者と観察されるものを分離するものであることがわかっています。戦略パターンは 2 つに似ており、分離の役割を果たすこともできますが、戦略の定義、作成、使用の 3 つの部分が分離されています。次に、完全な戦略パターンに含めるべき 3 つの部分について詳しく説明します。
1. ポリシーの定義
戦略クラスの定義は比較的単純で、戦略インターフェイスとこのインターフェイスを実装する戦略クラスのグループが含まれます。すべてのストラテジ クラスが同じインターフェイスを実装しているため、クライアント コードは、実装プログラミングではなくインターフェイスに基づいて、異なるストラテジを柔軟に置き換えることができます。サンプルコードは次のとおりです。
public interface Strategy {
void algorithmInterface();
}
public class ConcreteStrategyA implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
public class ConcreteStrategyB implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
2. ポリシーの作成
ストラテジー モードにはストラテジーのセットが含まれるため、それらを使用する場合、通常はタイプ (タイプ) を使用して、どのストラテジーを作成して使用するかを決定します。作成ロジックをカプセル化するには、作成の詳細をクライアント コードから保護する必要があります。タイプに基づいて戦略を作成するロジックを抽出し、それをファクトリ クラスに入れることができます。サンプルコードは次のとおりです。
public class StrategyFactory {
private static final Map<String, Strategy> strategies = new HashMap<>();
static {
strategies.put("A", new ConcreteStrategyA());
strategies.put("B", new ConcreteStrategyB());
}
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
return strategies.get(type);
}
}
一般に、ストラテジー クラスがステートレスで、メンバー変数を含まず、単なる純粋なアルゴリズムの実装である場合、そのようなストラテジー オブジェクトは共有して使用でき、getStrategy() を実行するたびに新しいものを作成する必要はありません。ポリシーオブジェクトから呼び出されます。この状況に対応して、上記のファクトリ クラスの実装を使用して、各ポリシー オブジェクトを事前に作成し、それをファクトリ クラスにキャッシュし、使用時に直接返すことができます。
逆に、ビジネス シナリオのニーズに従って、ポリシー クラスがステートフルである場合は、共有可能なポリシー オブジェクトをキャッシュするのではなく、ファクトリ メソッドから新しく作成されたポリシー オブジェクトを取得するたびに、次のようにする必要があります。ポリシー ファクトリ クラスは次のように実装されます。
public class StrategyFactory {
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
if (type.equals("A")) {
return new ConcreteStrategyA();
} else if (type.equals("B")) {
return new ConcreteStrategyB();
}
return null;
}
}
3. 戦略の使用
戦略の定義と作成について説明しましたが、次に戦略の使用について見てみましょう。
戦略パターンにはオプションの戦略のセットが含まれていることはわかっていますが、クライアント コードは通常、どの戦略を使用するかをどのように決定するのでしょうか? 最も一般的なのは、実行時に使用する戦略を動的に決定することであり、これは戦略パターンの最も典型的なアプリケーション シナリオでもあります。
ここでいう「実行時動的」とは、どの戦略が使用されるかは事前には分からないが、プログラムの実行中に設定、ユーザー入力、計算結果などの不確実な要素に応じて、どの戦略を使用するかを動的に決定することを意味します。次に、例を挙げて説明しましょう。
// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory
public class UserCache {
private Map<String, User> cacheData = new HashMap<>();
private EvictionStrategy eviction;
public UserCache(EvictionStrategy eviction) {
this.eviction = eviction;
}
//...
}
// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
public static void main(String[] args) throws Exception {
EvictionStrategy evictionStrategy = null;
Properties props = new Properties();
props.load(new FileInputStream("./config.properties"));
String type = props.getProperty("eviction_type");
evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}
// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
public static void main(String[] args) {
//...
EvictionStrategy evictionStrategy = new LruEvictionStrategy();
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}
上記のコードから、「非実行時動的決定」、つまり 2 番目のアプリケーションでの使用方法では、ストラテジ モードを利用できないこともわかります。このアプリケーション シナリオでは、戦略パターンは実際には「オブジェクト指向の多態性機能」または「実装ではなくインターフェイスに基づくプログラミング原則」に退化します。
ストラテジーモードを使って分岐判定を避けるにはどうすればいいですか?
実は分岐判定ロジックを削除できるモードはストラテジーモードだけではなく、後述するステートモードでも存在します。どのモードを使用するかは、アプリケーションのシナリオによって異なります。ストラテジ モードは、さまざまな種類のダイナミクスに応じてストラテジの使用が決定されるアプリケーション シナリオに適しています。
まずは例を用いて、if-elseまたはswitch-caseの分岐判定ロジックがどのように生成されるかを見てみましょう。具体的なコードは以下の通りです。この例では、戦略パターンを使用しませんでしたが、戦略の定義、作成、使用を直接結合しました。
public class OrderService {
public double discount(Order order) {
double discount = 0.0;
OrderType type = order.getType();
if (type.equals(OrderType.NORMAL)) { // 普通订单
//...省略折扣计算算法代码
} else if (type.equals(OrderType.GROUPON)) { // 团购订单
//...省略折扣计算算法代码
} else if (type.equals(OrderType.PROMOTION)) { // 促销订单
//...省略折扣计算算法代码
}
return discount;
}
}
分岐判定ロジックを削除するにはどうすればよいですか? そこで戦略パターンが役に立ちます。戦略パターンを使用して上記のコードをリファクタリングし、さまざまな種類の注文の割引戦略を戦略クラスに設計します。ファクトリ クラスは戦略オブジェクトの作成を担当します。具体的なコードは次のとおりです。
// 策略的定义
public interface DiscountStrategy {
double calDiscount(Order order);
}
// 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...
// 策略的创建
public class DiscountStrategyFactory {
private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();
static {
strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
}
public static DiscountStrategy getDiscountStrategy(OrderType type) {
return strategies.get(type);
}
}
// 策略的使用
public class OrderService {
public double discount(Order order) {
OrderType type = order.getType();
DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
return discountStrategy.calDiscount(order);
}
}
リファクタリング後のコードには if-else 分岐判定文がありません。実際、これはポリシー ファクトリー クラスのおかげです。ファクトリクラスでは、Mapを使用してストラテジをキャッシュし、型に応じてMapから直接対応するストラテジを取得することで、if-else分岐判定ロジックを回避しています。後で分岐判定ロジックを回避するために状態パターンを使用することについて説明するときに、同じルーチンを使用していることがわかります。本質的には、型分岐に基づいて判断するのではなく、型に基づいてテーブルを検索する「テーブル検索方式」を使用します (コード内の戦略はテーブルです)。
ただし、ビジネス シナリオで毎回異なるポリシー オブジェクトを作成する必要がある場合は、別のファクトリ クラス実装を使用する必要があります。具体的なコードは次のとおりです。
public class DiscountStrategyFactory {
public static DiscountStrategy getDiscountStrategy(OrderType type) {
if (type == null) {
throw new IllegalArgumentException("Type should not be null.");
}
if (type.equals(OrderType.NORMAL)) {
return new NormalDiscountStrategy();
} else if (type.equals(OrderType.GROUPON)) {
return new GrouponDiscountStrategy();
} else if (type.equals(OrderType.PROMOTION)) {
return new PromotionDiscountStrategy();
}
return null;
}
}
この実装は、元の if-else 分岐ロジックを OrderService クラスからファクトリ クラスに転送することに相当しますが、実際には削除されません。この問題の解決方法につきましては、本日をもちまして一時的に閉鎖させていただきます。メッセージエリアであなたの考えを話すことができます。それについては、次のクラスで説明します。
重要なレビュー
さて、今日の内容はここまでです。何に重点を置く必要があるのか、一緒にまとめて見直してみましょう。
戦略パターンは、アルゴリズム クラスのファミリーを定義し、各アルゴリズムを個別にカプセル化して、相互に置き換えることができるようにします。Strategy パターンは、アルゴリズムを使用するクライアントとは独立してアルゴリズムを変更できます (ここでのクライアントとは、アルゴリズムを使用するコードを指します)。
戦略パターンは、戦略の定義、作成、使用を分離するために使用されます。実際、完全な戦略パターンはこれら 3 つの部分で構成されます。
- 戦略クラスの定義は比較的単純で、戦略インターフェイスとこのインターフェイスを実装する戦略クラスのグループが含まれます。
- ポリシーの作成は、ポリシー作成の詳細をカプセル化するファクトリ クラスによって行われます。
- ストラテジ モードには、オプションのストラテジのセットが含まれています。クライアント コードが使用するストラテジを選択する方法には、コンパイル時の静的決定と実行時の動的決定の 2 つの方法があります。その中でも、「実行時の動的決定」は、ストラテジーパターンの最も典型的な適用シナリオです。
また、ストラテジーモードによりif-else分岐判定を削除することも可能です。実際、これはストラテジー ファクトリ クラスの恩恵を受けており、より本質的には、「ルックアップ テーブル メソッド」を使用して、型ブランチに基づく判断を型ルックアップ テーブルの助けを借りて置き換えます。
クラスディスカッション
今日は、ストラテジ ファクトリ クラスで、毎回新しいストラテジ オブジェクトを返す必要がある場合でも、ファクトリ クラスで if-else 分岐判定ロジックを記述する必要があると述べましたが、この問題をどのように解決するか?