策略设计模式

策略设计模式(Strategy Pattern)是行为型设计模式之一,用于在一个系统中定义一系列可互换的算法或行为,并使它们可以在运行时动态地互相替换。使用策略模式可以避免直接将多个算法硬编码在类中,提升系统的灵活性和可扩展性。

策略模式的组成

策略模式的核心思想是将行为从上下文类中抽离出来,使这些行为可以相互替换,避免冗长的条件判断语句。策略模式的主要组成部分包括:

  1. Context(上下文类)

    • 上下文类是用户和策略的桥梁,它包含对 Strategy 对象的引用,并通过这个引用来执行策略。
    • Context 负责和客户端交互,客户端通过它来调用不同的策略。
    • 上下文类并不关心策略的具体实现细节,只要策略符合接口要求,它就可以正常运行。
  2. Strategy(策略接口)

    • 策略接口定义了一系列算法或行为的抽象。
    • 具体策略类都需要实现这个接口,以确保它们可以在上下文中被替换和使用。
  3. ConcreteStrategy(具体策略)

    • 具体策略类实现了 Strategy 接口中的方法。
    • 每一个具体策略类实现了一种特定的算法。

策略模式的 UML 类图

策略模式的类图如下所示:

在这个类图中,Context 类与 Strategy 接口有关联关系,ConcreteStrategyAConcreteStrategyB 是不同的策略实现类。

策略模式的优点

  1. 符合开闭原则

    • 策略模式遵循开闭原则(Open-Closed Principle),即对于扩展是开放的,而对于修改是封闭的。当需要增加新的折扣策略时,只需添加新的策略类,而不必修改上下文类或其他已有策略类。
  2. 消除条件判断

    • 策略模式消除了在 Context 中使用大量的 if-elseswitch-case 判断来选择算法的需要。通过将这些行为抽象为不同的策略类,每个策略类只负责一件事。
  3. 提高代码复用

    • 不同的策略类实现了相同的接口,可以在不同的上下文环境中复用这些策略类,使代码更加简洁和可维护。
  4. 提高灵活性

    • 在运行时可以根据需要动态地更换不同的策略。例如,在电商系统中,我们可以根据客户类型来动态地设置折扣策略。

策略模式的缺点

  1. 客户端必须了解所有策略

    • 使用策略模式时,客户端需要知道并了解不同的策略实现类,这可能会增加客户端的复杂性。
  2. 可能导致类数目增加

    • 每一个策略都需要一个单独的类,因此可能会增加系统中类的数目,尤其是在策略较多的情况下。
  3. 上下文对客户端的依赖

    • 上下文需要持有策略接口的引用,意味着上下文必须明确某个策略的类型。如果策略的配置逻辑复杂,那么会给上下文类增加一定的负担。

策略模式的适用场景

  1. 多个类只有行为上的差异

    • 当你有许多类的行为有不同实现时,可以考虑使用策略模式将这些行为抽象出来,封装成策略。
  2. 需要在运行时动态选择算法

    • 当一个系统的某些功能需要在运行时根据具体情况选择不同的实现时,例如支付方式选择、日志记录策略等。
  3. 避免条件判断的蔓延

    • 当你发现代码中存在大量的 if-elseswitch-case 语句,并且这些分支用于处理不同的行为时,可以考虑使用策略模式来代替。

Java 示例讲解

我们再来看一个更详细的 Java 示例。假设你要开发一个计算折扣的系统,不同类型的客户(如普通客户、会员客户、VIP 客户)有不同的折扣策略。我们可以使用策略模式来实现这个折扣系统。

1. 定义策略接口

我们首先定义一个 DiscountStrategy 接口,里面包含一个计算折扣价格的方法。

// 定义折扣策略的接口
public interface DiscountStrategy {
    double calculateDiscount(double price);
}

2. 实现具体策略

接下来,我们为不同类型的客户实现具体的折扣策略。

// 普通客户没有折扣
public class NoDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double price) {
        return price;
    }
}

// 会员客户享受 10% 的折扣
public class MemberDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double price) {
        return price * 0.9;
    }
}

// VIP 客户享受 20% 的折扣
public class VIPDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double price) {
        return price * 0.8;
    }
}

在这些具体的策略类中:

  • NoDiscountStrategy:表示没有折扣的策略。
  • MemberDiscountStrategy:表示会员客户享受 10% 的折扣。
  • VIPDiscountStrategy:表示 VIP 客户享受 20% 的折扣。

3. 创建上下文类

然后,我们创建一个 Context 类,即上下文类,用于持有一个 DiscountStrategy 的引用,并通过它来计算折扣价格。

public class PriceContext {
    private DiscountStrategy discountStrategy;

    // 设置折扣策略
    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    // 计算最终价格
    public double getDiscountedPrice(double price) {
        if (discountStrategy == null) {
            throw new IllegalStateException("Discount strategy is not set.");
        }
        return discountStrategy.calculateDiscount(price);
    }
}

4. 测试策略模式

接下来,我们测试一下这个策略模式的应用。

public class StrategyPatternExample {
    public static void main(String[] args) {
        PriceContext context = new PriceContext();

        // 原价为 100
        double originalPrice = 100.0;

        // 使用无折扣策略
        context.setDiscountStrategy(new NoDiscountStrategy());
        System.out.println("Normal Customer Price: " + context.getDiscountedPrice(originalPrice));

        // 使用会员折扣策略
        context.setDiscountStrategy(new MemberDiscountStrategy());
        System.out.println("Member Customer Price: " + context.getDiscountedPrice(originalPrice));

        // 使用 VIP 折扣策略
        context.setDiscountStrategy(new VIPDiscountStrategy());
        System.out.println("VIP Customer Price: " + context.getDiscountedPrice(originalPrice));
    }
}

输出结果为:

Normal Customer Price: 100.0
Member Customer Price: 90.0
VIP Customer Price: 80.0

策略设计模式是一种强大且灵活的设计模式,能够帮助开发人员将不同的算法和行为封装为独立的策略类。它使得代码更加符合开闭原则,并消除了复杂的条件判断逻辑。通过策略模式,可以动态选择不同的策略类来解决特定的问题,使得系统更加灵活、可扩展,便于维护。

SpringBoot使用这种策略模式

在 Spring Boot 中,策略设计模式可以很好地解决多种行为的动态选择问题,比如根据不同的业务需求调用不同的算法或服务。在 Spring 框架中,通过依赖注入和 Spring 容器管理 bean,可以非常自然地实现策略模式。以下是如何在 Spring Boot 中运用策略设计模式的详细讲解。

策略的标识不一定是用户传输的,可以是后端在本地缓存中获取的,确保用户所使用的策略不被轻易篡改

1. 定义策略接口

我们首先定义一个策略接口 DiscountStrategy,它定义了计算折扣的方法。

public interface DiscountStrategy {
    double calculateDiscount(double price);
}

2. 实现具体的策略类

接着为每个用户类型实现具体的折扣策略。这里我们有三种策略:普通用户、会员用户、VIP 用户。

import org.springframework.stereotype.Component;

@Component("noDiscount")
public class NoDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double price) {
        return price;
    }
}

@Component("memberDiscount")
public class MemberDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double price) {
        return price * 0.9;
    }
}

@Component("vipDiscount")
public class VIPDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double price) {
        return price * 0.8;
    }
}

3. 创建上下文类

为了管理这些策略并在运行时选择适当的策略,我们创建一个上下文类 DiscountContext,通过依赖注入来实现动态选择。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class DiscountContext {

    private final Map<String, DiscountStrategy> discountStrategyMap;

    @Autowired
    public DiscountContext(Map<String, DiscountStrategy> discountStrategyMap) {
        this.discountStrategyMap = discountStrategyMap;
    }

    public double executeDiscountStrategy(String strategyType, double price) {
        DiscountStrategy strategy = discountStrategyMap.get(strategyType);
        if (strategy == null) {
            throw new IllegalArgumentException("Invalid discount type: " + strategyType);
        }
        return strategy.calculateDiscount(price);
    }
}

解释:

  • @Autowired:Spring 自动注入所有 DiscountStrategy 的实现,并将它们放入一个 Map,key 是 bean 的名称,value 是 bean 对象。
  • executeDiscountStrategy 方法:根据传入的策略名称,获取相应的策略对象并执行计算。

4. 创建控制器来测试策略模式

我们创建一个简单的控制器来测试策略模式的应用。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DiscountController {

    @Autowired
    private DiscountContext discountContext;

    @GetMapping("/discount")
    public String getDiscount(
            @RequestParam String userType, 
            @RequestParam double price) {
        double discountedPrice = discountContext.executeDiscountStrategy(userType, price);
        return "Discounted price for " + userType + ": " + discountedPrice;
    }
}

解释:

  • @RestController:定义一个 RESTful 控制器。
  • @GetMapping("/discount"):提供一个 GET 接口 /discount,接受 userTypeprice 参数,根据用户类型计算折扣后的价格。

5. 运行测试

启动 Spring Boot 应用后,可以通过以下 URL 来测试:

  • 普通用户(没有折扣):http://localhost:8080/discount?userType=noDiscount&price=100
  • 会员用户(10% 折扣):http://localhost:8080/discount?userType=memberDiscount&price=100
  • VIP 用户(20% 折扣):http://localhost:8080/discount?userType=vipDiscount&price=100

结果示例:

  • 普通用户:Discounted price for noDiscount: 100.0
  • 会员用户:Discounted price for memberDiscount: 90.0
  • VIP 用户:Discounted price for vipDiscount: 80.0

Map注入讲解

在 Spring 中,使用 Map 注入是一种非常灵活的方式,可以方便地管理多种实现类型。在很多业务场景中,我们需要根据一些条件动态选择不同的实现。通过 Map 注入,我们可以轻松实现策略模式,并利用 Spring 容器自动管理所有实现的优势。

什么是 Map 注入?

在 Spring 中,Map 注入是一种将多个相同接口的实现类注入到一个 Map 集合中的方式。通过这种方式,我们可以轻松地访问和使用 Spring 容器中的所有实现类,并通过 Map 的键来动态选择具体的实现。

当我们使用 Map<String, BeanType> 进行注入时,Spring 会自动将所有符合条件的 Bean 以键值对的形式注入到 Map 中,其中:

  • String 是 Bean 的名称(默认为类名的小写,如果使用了 @Component("customName"),则为指定的名称)。
  • BeanType 是实现该接口的具体 Bean 实例。

使用场景

  • 策略模式:不同的策略实现会被注入到 Map 中,可以根据传入的键动态选择策略。
  • 动态业务逻辑:在业务逻辑中,有些方法需要根据不同的业务类型调用不同的实现。
  • 避免条件判断:使用 Map 注入,可以避免复杂的 if-elseswitch 判断,直接通过键查找实现类。

Map 注入工作原理

  1. Spring 容器管理所有 Bean

    • Spring 会扫描 @Component 注解标识的类,将这些类作为 Bean 注册到 Spring 容器中。
    • 如果一个类实现了 DiscountStrategy 接口,并被标识为 @Component("beanName"),那么它将作为一个具体的策略 Bean 被注册。
  2. 将实现注入到 Map

    • 当 Spring 看到需要注入一个 Map<String, DiscountStrategy> 时,它会找到所有实现了 DiscountStrategy 接口的 Bean,然后将它们注入到 Map 中。
    • Map 中,key 为 @Component 注解指定的名称(如果没有指定,则为类名的小写),value 为具体的 Bean 实例。
  3. 动态选择策略

    • 在上下文类中,我们可以使用传入的键(如 "memberDiscount")从 Map 中获取对应的策略,然后调用其方法。
    • 这样可以避免使用 if-elseswitch 来选择不同的策略。

优势

  1. 代码简洁,避免条件判断

    • 通过将所有实现注入到 Map 中,可以根据键值动态选择策略,避免了复杂的 if-elseswitch 判断逻辑。
  2. 易于扩展

    • 如果需要增加新的策略,只需要添加一个实现了 DiscountStrategy 接口的新类,并用 @Component 注解标识即可。Spring 会自动将其加入到 Map 中,无需修改其他代码。
  3. 符合开闭原则

    • 新的策略可以独立添加,不需要修改现有代码,从而符合开闭原则(Open-Closed Principle)。
  4. 高内聚,低耦合

    • 每个策略类只实现具体的逻辑,不依赖其他策略类,具有高内聚和低耦合的优点。上下文类 DiscountContext 只负责管理和选择策略,并不关心具体策略的实现细节。

使用注意事项

  1. Bean 名称唯一

    • Map 注入的键是 Bean 的名称,因此每个策略类的 Bean 名称必须是唯一的。如果没有显式指定名称,Spring 会使用默认名称(通常是类名的小写形式)。在需要多个实现的情况下,确保 Bean 名称不重复。
  2. 注入的 Map 类型

    • 确保 Map 的键类型为 String,值为接口类型(如 DiscountStrategy)。这样可以让 Spring 自动注入正确的实现类。
  3. 异常处理

    • 当通过键查找策略时,要处理 Map 中不存在键的情况。比如在 executeDiscountStrategy() 方法中,如果找不到相应的策略,要抛出适当的异常。