跳到主要内容

2024-10-17

一言

我为自己而死,并且,在最后的最后,为自己而活。 --- 《SCP基金会》 · abc2237512422


策略模式

策略模式简介

策略模式(Strategy Pattern)是一种行为型设计模式,定义了一系列算法或行为,把它们分别封装起来,使得它们可以相互替换。策略模式让算法或行为在不影响客户端的情况下发生变化,主要用于从一组算法中选择一个适合的算法。

在策略模式中,有三种主要角色:

  1. 策略接口(Strategy Interface):定义了一系列可供选择的算法或行为。不同的算法需要实现这个接口。
  2. 具体策略类(Concrete Strategy):实现策略接口的不同版本,每个类代表一个独立的算法或行为。
  3. 上下文类(Context):它维护一个对策略对象的引用,并根据客户端的需求调用不同的策略。

策略模式的结构

策略模式的 UML 类图如下:

+-------------------+         +----------------------+
| Context | | Strategy |
|-------------------|<--------|----------------------|
| - strategy: S | | + algorithmInterface()|
| + executeStrategy()| +----------------------+
+-------------------+ ^
/ \
/ \
/ \
+-------------------+ +-------------------+
|ConcreteStrategyA | |ConcreteStrategyB |
|-------------------| |-------------------|
|+ algorithmInterface()| |+ algorithmInterface()|
+-------------------+ +-------------------+

各个组成部分:

  1. Context(上下文类):上下文持有策略接口的引用,可以动态地设置或更改策略对象。上下文不关注具体的算法实现,而是委托给策略对象执行。
  2. Strategy(策略接口):定义了所有策略必须实现的接口。策略接口通常定义一个通用的行为方法(如 algorithmInterface()),具体的实现由各个策略类提供。
  3. ConcreteStrategy(具体策略):每个具体的策略类都实现了策略接口中的方法,封装了具体的算法。

策略模式的示例

下面以一个支付系统为例,用户可以选择不同的支付方式(比如信用卡、PayPal、比特币),这些支付方式就是策略,用户可以根据不同情况选择对应的策略。

1. 策略接口

首先定义一个 PaymentStrategy 接口,代表不同的支付方式策略:

public interface PaymentStrategy {
void pay(int amount);
}

2. 具体策略类

接下来我们为不同的支付方式创建具体的策略类,比如 CreditCardStrategyPayPalStrategyBitcoinStrategy,它们分别实现了 PaymentStrategy 接口。

// 信用卡支付策略
public class CreditCardStrategy implements PaymentStrategy {

private String name;
private String cardNumber;

public CreditCardStrategy(String name, String cardNumber) {
this.name = name;
this.cardNumber = cardNumber;
}

@Override
public void pay(int amount) {
System.out.println(amount + " paid with credit card.");
}
}

// PayPal支付策略
public class PayPalStrategy implements PaymentStrategy {

private String email;

public PayPalStrategy(String email) {
this.email = email;
}

@Override
public void pay(int amount) {
System.out.println(amount + " paid using PayPal.");
}
}

// 比特币支付策略
public class BitcoinStrategy implements PaymentStrategy {

private String bitcoinAddress;

public BitcoinStrategy(String bitcoinAddress) {
this.bitcoinAddress = bitcoinAddress;
}

@Override
public void pay(int amount) {
System.out.println(amount + " paid using Bitcoin.");
}
}

3. 上下文类

接着,我们定义一个上下文类 ShoppingCart,它将使用不同的支付策略来完成支付:

public class ShoppingCart {

private PaymentStrategy paymentStrategy;

// 动态设置支付策略
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}

// 执行支付操作
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}

4. 客户端调用

现在,客户端可以通过上下文类 ShoppingCart 来选择不同的支付方式:

public class StrategyPatternDemo {

public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();

// 用户选择使用信用卡支付
cart.setPaymentStrategy(new CreditCardStrategy("John Doe", "1234-5678-9876"));
cart.checkout(100); // 输出: 100 paid with credit card.

// 用户选择使用PayPal支付
cart.setPaymentStrategy(new PayPalStrategy("john@example.com"));
cart.checkout(200); // 输出: 200 paid using PayPal.

// 用户选择使用比特币支付
cart.setPaymentStrategy(new BitcoinStrategy("1BitcoinAddress"));
cart.checkout(300); // 输出: 300 paid using Bitcoin.
}
}

在这个例子中,ShoppingCart 类不知道也不关心具体的支付方式,它只是根据设置的策略来执行支付操作。支付策略可以灵活地被替换或扩展,添加新支付方式时不需要修改现有的 ShoppingCart 类,符合开闭原则。

策略模式的优缺点

优点:

  1. 避免了冗长的 if-elseswitch-case 语句:将算法或行为的选择交给策略类来处理,避免大量的条件分支。
  2. 遵循开闭原则:可以在不修改原有代码的基础上扩展新的策略,增加新的行为或算法。
  3. 提高代码的灵活性和可维护性:将算法或行为的实现独立到不同的类中,每个类只处理一种具体的算法,职责明确。
  4. 简化代码:将复杂的逻辑分解为多个策略类,客户端代码只需选择合适的策略。

缺点:

  1. 增加类的数量:每个策略都需要定义一个类,导致类的数量增加。
  2. 客户端需要知道所有策略的区别:策略模式将算法选择的责任交给了客户端,客户端需要了解不同策略的特性以作出合适的选择。
  3. 策略类可能会有重复代码:不同策略类之间可能会有一些相同的逻辑,这样会导致代码的重复。

策略模式的使用场景

策略模式适用于以下场景:

  1. 需要动态选择算法或行为时:如支付系统、排序算法的选择等。
  2. 希望避免冗长的条件判断语句:使用策略模式可以将 if-elseswitch-case 结构替换为策略类。
  3. 不同算法或行为之间可以互相替换:不同策略类的行为可以灵活替换,而不影响客户端的代码。
  4. 需要扩展新行为时不希望修改现有代码:在新需求下,只需增加新的策略类,而不需要修改已有的上下文和策略类,符合开闭原则。

总结

策略模式通过将不同的算法或行为封装到独立的策略类中,避免了复杂的条件分支语句,增强了代码的灵活性和可扩展性。策略模式尤其适用于行为或算法经常变化、需要灵活扩展的场景,是一种面向对象设计中常用的设计模式之一。

开闭原则

你说的没错,在增加一个新的支付方式(新的策略类)时,确实需要编写这个新支付方式的策略类,并且在某些场景下,调用代码可能需要知道新支付方式的存在。这似乎与开闭原则(OCP,Open-Closed Principle)有一定的矛盾,但其实两者并不冲突。

理解“开闭原则”

开闭原则的核心思想是:

  • 对扩展开放:可以通过扩展系统的行为,增加新功能(如新增支付方式),而无需修改已有的系统。
  • 对修改关闭:不需要为了增加新功能而修改现有的代码逻辑。

在策略模式中,原有的上下文类 (ShoppingCart) 不需要修改来支持新的支付方式,这是策略模式符合开闭原则的地方。也就是说,你在引入新的支付策略时,无需修改 ShoppingCart 或现有的策略类,只需要创建一个新的策略类,符合“对修改关闭”的原则。

增加新支付方式的具体情况

  1. 新增支付策略类:当你增加一个新的支付方式时,需要创建一个新的策略类(如 ApplePayStrategy),这符合开闭原则中的“对扩展开放”。

    例如,新增一个 ApplePayStrategy 类:

    public class ApplePayStrategy implements PaymentStrategy {
    @Override
    public void pay(int amount) {
    System.out.println(amount + " paid using ApplePay.");
    }
    }
  2. 使用新的支付方式:如果客户端想要使用新的支付方式,那么它需要将新的支付策略对象传递给上下文类。比如在客户端代码中设置 ApplePayStrategy

    cart.setPaymentStrategy(new ApplePayStrategy());
    cart.checkout(400); // 输出: 400 paid using ApplePay.

    从这个角度来看,客户端代码确实需要改动,但是这种改动属于应用层逻辑的扩展,而不是对现有系统的核心逻辑的修改。你并没有修改原有的 ShoppingCart 或已有的支付策略类。这个改动的本质是因为你需要使用新的策略,而不是改变现有的代码结构。

开闭原则的实际应用

在策略模式下,已有代码的稳定性得到了保障。你可以在不修改已有代码(如 ShoppingCart 和已有的支付策略类)的情况下增加新的支付方式。增加新策略类的代码其实属于扩展功能,符合开闭原则中的“对扩展开放”。

当然,客户端代码确实需要知道新增的支付方式,以便在业务逻辑中使用它。但这并不破坏开闭原则,因为开闭原则针对的是系统的核心逻辑,而不是业务逻辑上的自然扩展。

进一步优化:配置驱动

为了更好地体现开闭原则,如果不想在客户端代码中显式引用新策略,可以通过配置驱动的方式来选择策略。例如,使用反射或者通过依赖注入(DI)容器(如 Spring)来动态加载策略:

  1. 通过反射动态加载策略类: 可以通过配置文件指定支付方式,使用反射根据类名动态加载策略,避免显式地在客户端代码中创建策略实例。

  2. 使用依赖注入(DI): 你可以将支付策略通过依赖注入的方式提供给 ShoppingCart,并让 ShoppingCart 动态选择合适的策略,而无需手动修改客户端代码。

    例如,使用 Spring 容器注入所有策略:

    @Autowired
    private Map<String, PaymentStrategy> strategies;

    public void checkout(int amount, String paymentMethod) {
    PaymentStrategy strategy = strategies.get(paymentMethod);
    if (strategy != null) {
    strategy.pay(amount);
    } else {
    throw new IllegalArgumentException("Unknown payment method");
    }
    }

    在这种方式下,增加新的支付方式时,客户端无需修改代码,只需在 Spring 配置中注册新的支付策略即可。

总结

虽然在增加新支付方式时,客户端代码可能需要知道新策略的存在并使用它,但这并不违反开闭原则,因为:

  1. 你没有修改原有的系统核心代码,比如上下文类或已有的策略类。
  2. 增加新支付方式的策略类属于功能的自然扩展,而不是对现有功能的修改。

策略模式通过将不同的行为封装在独立的策略类中,遵循了开闭原则的核心思想:在不修改已有代码的情况下,扩展系统的功能。通过配置驱动、依赖注入等手段,还可以减少对客户端代码的修改,使系统更加灵活。