当前位置: 首页 > article >正文

【策略模式】最佳实践——Spring IoC实现策略模式全流程深度解析

简介

策略模式是一种行为型设计模式,它定义了一系列算法,并将每一个算法封装起来,使它们可以互相替换,并且使算法的变化不会影响使用算法的客户端。策略模式通过将具体的业务逻辑从上下文(Context)中剥离出来,独立为策略类,动态地将所需的行为注入上下文对象中,从而避免代码中充斥条件判断逻辑。

策略模式的核心由以下三部分组成:

  • 策略接口(Strategy Interface):定义所有具体策略类的公共接口,用于约束具体策略的实现。
  • 具体策略类(Concrete Strategy):实现策略接口,在内部封装具体的算法和业务逻辑。
  • 上下文类(Context):持有一个具体策略对象的引用,用来调用具体策略类的方法。也可以使用工厂模式,将创建具体策略类的动作交给工厂类执行,实现业务逻辑、创建逻辑与应用程序的解耦合。

在下面的文章中,我将给策略模式的实现分为三步,从业务逻辑解耦创建逻辑解耦满足开闭原则,一步步实现策略模式的最佳实践

1.策略模式(业务逻辑解耦)

设计一个支付系统,支持多种支付方式,包括支付宝(Alipay)、微信支付(WeChatPay)和银行卡支付(BankCardPay)。不同支付方式的逻辑独立,同时系统可以根据需求动态切换支付方式。

定义策略接口

// 定义支付策略接口
public interface PaymentStrategy {
    void pay(double amount); // 支付方法
}

定义具体策略类

不同的具体策略类都统一继承策略接口,在类中重写策略接口定义的方法,编写具体的业务逻辑。

// 支付宝支付策略
public class AlipayStrategy implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("使用支付宝支付了: ¥" + amount);
    }
}

// 微信支付策略
public class WeChatPayStrategy implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("使用微信支付了: ¥" + amount);
    }
}

// 银行卡支付策略
public class BankCardStrategy implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("使用银行卡支付了: ¥" + amount);
    }
}

定义上下文类

在上下文中定义策略类的引用,使用 set 方法注入具体策略类,通过方法调用执行具体策略类的业务逻辑。

// 支付上下文类
public class PaymentContext {
    private PaymentStrategy strategy; // 策略引用

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

    // 执行支付
    public void pay(double amount) {
        if (strategy == null) {
            throw new IllegalStateException("未设置支付策略!");
        }
        strategy.pay(amount); // 委托给具体策略
    }
}

使用上下文类

在用户选用具体的支付方式时,需要给上下文 context 传入具体策略的对象,才可使用该策略的支付方法。(这种方式需要应用程序自己手动 new 出具体策略的对象,并且通过 set 方法传入给上下文类)

虽然通过这种方式,可以使得具体的支付业务逻辑与应用程序解耦合。但是依然存在两个问题:

  1. 具体策略类的创建逻辑仍然由应用程序执行,即策略类创建逻辑仍然与应用程序耦合
  2. 如果需要添加新的策略,就需要再添加一个新的 if-else 分支,因此不符合开闭原则。(开闭原则通俗点说就是在添加新的功能时,不能修改现有的代码,这里添加一个新的分支就属于修改了现有的代码)
public class Main {
    public static void main(String[] args) {
        String type = "Alipay";
        PaymentContext context = new PaymentContext();

        if (type.equals("Alipay")) {
            // 用户使用支付宝支付
            context.setPaymentStrategy(new AlipayStrategy());
            context.pay(15);
        } else if (type.equals("Wechat")) {
            context.setPaymentStrategy(new WeChatPayStrategy());
            context.pay(10);
        } else {
            context.setPaymentStrategy(new BankCardStrategy());
            context.pay(5);
        }
    }
}

执行结果如下:

开闭原则: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

对扩展开放(Open for extension):软件实体应该允许在不改变其现有代码的情况下,通过增加新功能来对其进行扩展。也就是说,当软件的需求发生变化时,我们应该能够通过添加新代码来满足这些需求,而不需要修改已有的代码。

对修改关闭(Closed for modification):一旦软件实体被开发完成并投入使用,其源代码就不应该再被修改。这可以防止对现有功能的破坏,减少引入新的错误的风险,并使软件更加稳定和可维护。

2.引入工厂模式(创建逻辑解耦)

通过引入工厂模式,可以将创建具体策略类的动作转移给工厂类执行,将策略类的创建逻辑与应用程序解耦合

定义工厂类

由工厂类负责策略类的创建逻辑,根据传入的不同参数,创建不同的具体策略类。这一步骤也就实现了策略类的创建逻辑与应用程序的解耦。

// 工厂类
public class PaymentStrategyFactory {
    public static PaymentStrategy getStrategy(String type) {
        switch (type) {
            case "Alipay":
                return new AlipayStrategy();
            case "WeChatPay":
                return new WeChatPayStrategy();
            case "BankCard":
                return new BankCardStrategy();
            default:
                throw new IllegalArgumentException("无效的支付类型");
        }
    }
}

改造工厂类

可以发现,如果需要使用大量的具体策略类的话,应用程序就需要编写大量的 if-elseswitch 分支。我们可以使用 Map 存储具体策略类,从而取消分支判断的设计。

public class PaymentStrategyFactory {
    // 定义Map集合
    private static final Map<String, PaymentStrategy> map = new HashMap<>();
    //
    static {
        map.put("Alipay", new AlipayStrategy());
        map.put("WeChatPay", new WeChatPayStrategy());
        map.put("BankCard", new BankCardStrategy());
    }

    public static PaymentStrategy getStrategy(String type) {
        // 如果存在该支付类型,返回对应的策略类
        if (map.containsKey(type)) return map.get(type);
        // 如果不存在该支付类型,抛出异常
        else throw new IllegalArgumentException("无效的支付类型");
    }
}

通过上述的改造,虽然解决了创建逻辑与应用程序耦合的问题,但是不符合开闭原则的问题仍然没有解决(如果需要添加新的具体策略类,还是需要修改工厂类的代码,在 Map 集合中 put 一个新的键值对)。

比如,如果要添加一个苹果支付的业务逻辑,就需要在 static 中添加这个代码,从而打破了开闭原则。

map.put("ApplePay", new ApplePayStrategy());

下文我们将使用 Spring IoC 解决这个问题。

使用工厂类

直接使用 PaymentStrategyFactory 工厂类的静态方法 getStrategy() 获取策略对象,调用策略对象的方法即可。

public class Main {
    public static void main(String[] args) {
        PaymentStrategy alipay = PaymentStrategyFactory.getStrategy("Alipay");
        alipay.pay(10);
    }
}

执行结果如下:

3.引入Spring IoC(满足开闭原则)

改造策略接口

需要在策略接口 PaymentStrategy 添加一个新的 mark 方法,用于标识每个接口。

public interface PaymentStrategy {
    void pay(double amount); // 支付方法

    String mark();	// 标识方法
}

改造具体策略类

具体策略类由于实现了策略接口,因此需要重写标识方法,这里直接返回对应策略的标识即可。(这一步骤很重要,应用程序通过工厂类获取策略类,就是通过这个标识获取的)

此外,我们还需要给每个具体策略类添加 @Component 注解,方便 Spring 容器管理具体策略类的生命周期。

通过 @Component 注解,实际上是通过 Spring 容器来执行 new 的步骤,也就是将具体策略类的创建逻辑由工厂类交给了 Spring 容器

@Component:可以标记任意类为 Spring Bean,Spring 容器会自动扫描和管理使用 @Component 注解标注的类。

支付宝支付策略:

@Component
public class AlipayStrategy implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("使用支付宝支付了: ¥" + amount);
    }

    // 返回策略的标识
    @Override
    public String mark() {
        return "Alipay";
    }
}

微信支付策略:

@Component
public class WeChatPayStrategy implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("使用微信支付了: ¥" + amount);
    }

    @Override
    public String mark() {
        return "WeChatPay";
    }
}

银行卡支付策略:

@Component
public class BankCardStrategy implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("使用银行卡支付了: ¥" + amount);
    }

    @Override
    public String mark() {
        return "BankCard";
    }
}

改造工厂类

下面开始重头戏,我们将对工厂类进行改造,通过 Spring 容器帮我们管理策略类。

@Component
public class PaymentStrategyFactory implements InitializingBean {
    // 注入IoC容器
    @Autowired
    private ApplicationContext context;

    // 定义Map集合
    private final Map<String, PaymentStrategy> strategyMap = new HashMap<>();

    // 获取对应标识的策略类
    public PaymentStrategy getStrategy(String type) {
        if (strategyMap.containsKey(type)) return strategyMap.get(type);
        else throw new IllegalArgumentException("无效的支付类型");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 从IoC容器中获取所有策略接口 PaymentStrategy 的实现类,即具体策略类
        Map<String, PaymentStrategy> beansOfType = context.getBeansOfType(PaymentStrategy.class);

        beansOfType.forEach((key, value) -> strategyMap.put(value.mark(), value));
    }
}
  1. 添加注解:首先需要给工厂类添加 @Component 注解,后续我们将通过依赖注入的方式使用该工厂类。
@Component
  1. 实现 InitializingBean 接口:通过重写该接口的 afterPropertiesSet 方法,可以在 Bean 的初始化阶段将 Spring 容器创建好的具体策略类写入到工厂类的 strategyMap 集合中。

Spring Bean 的生命周期:整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。

public class PaymentStrategyFactory implements InitializingBean
  1. 注入 IoC 容器:使用 @Autowired 注解注入 ApplicationContext,即 IoC 容器。方便后续从容器中获取 Bean 名称和策略接口实现类的 Map 集合。
@Autowired
private ApplicationContext context;
  1. 定义 Map 集合:这个 Map 是工厂类存放策略类的集合,应用程序将从这个集合中获取对应的策略类。
// 定义Map集合
private final Map<String, PaymentStrategy> strategyMap = new HashMap<>();
  1. 编写 afterPropertiesSet 方法逻辑:这里我们需要将 Bean 名称和策略类的键值对转化为标识和策略类的键值对,方便我们根据传入的标识直接获取策略类。
@Override
public void afterPropertiesSet() throws Exception {
    // 4.1.从IoC容器中获取所有策略接口 PaymentStrategy 的实现类,即具体策略类
    Map<String, PaymentStrategy> beansOfType = context.getBeansOfType(PaymentStrategy.class);
    // 4.2.利用lambda表达式,将Spring容器中的策略类转移到工厂类的Map集合中
    beansOfType.forEach((key, value) -> strategyMap.put(value.mark(), value));
}

第一步先从注入的 Spring 容器中获取容器创建好策略类的 Map 集合,该集合存储的是 Bean 名称和策略类的键值对,如下图:

第二步使用了 lambda 表达式(如果不熟练的话使用普通 for 循环也行),将Spring容器中的策略类转移到工厂类的Map集合中,其实这一步也就是将 key 值简化,方便根据标识字符串获取策略类。如下图:

  1. 编写 getStrategy 方法逻辑:最后一步,可以从 strategyMap 集合中获取对应的策略类了。
// 获取对应标识的策略类
public PaymentStrategy getStrategy(String type) {
    if (strategyMap.containsKey(type)) return strategyMap.get(type);
    else throw new IllegalArgumentException("无效的支付类型");
}

工厂类到这里就彻底改造完毕了,我们通过将每个具体策略类都添加了 @Component 注解,Spring 启动时就会将这些使用了 @Component 注解的类创建出来并添加到 Spring IoC 容器中。这样,在添加新的策略类时就无需修改原有的工厂类,满足了开闭原则。

使用工厂类

@SpringBootTest
public class StrateTest {
	// 注入工厂类
    @Autowired
    private PaymentStrategyFactory factory;

    @Test
    public void testStrategy() {
        PaymentStrategy alipay = factory.getStrategy("Alipay");
        alipay.pay(1000);
    }
}

执行结果如下:

总结

最佳实践小结

通过上文的三个步骤,我们一步步将策略模式改造为了最佳实践,实现了业务逻辑、创建逻辑与应用程序的解耦以及满足了开闭原则。在Spring IoC 的策略模式实现中,主要做了这三件事情:

  1. 业务逻辑与应用程序解耦:通过定义策略接口,规范不同具体策略类的统一实现,将算法的业务逻辑交给了策略类实现。
  2. 创建逻辑与应用程序解耦:通过给策略类添加 @Component 注解,由 Spring 扫描并注册具体策略类的对象到 Spring 容器中,从而将策略类的创建逻辑转移到了 Spring 容器。
  3. 满足开闭原则: 由于添加了 @Component 注解的策略类在 Spring 启动后都被注册到了 Spring 容器中,无需开发者手动硬编码到工厂类。在需要添加新的策略类时,Spring 容器可以帮助开发者自动注入。

对比其他模式

策略模式

  • 定义:定义一系列算法,将每种算法封装在独立的策略类中,使得这些算法可以互相替换,且算法的变化不会影响使用算法的上下文对象。
  • 核心思想动态选择算法,运行时可以替换策略对象。

模板方法模式

  • 定义:定义一个算法的框架,将算法的某些步骤延迟到子类中实现,使得子类可以在不改变算法结构的情况下重新定义算法的某些步骤。
  • 核心思想固定流程,子类负责填充或改写部分步骤。

工厂模式

  • 定义:定义一个用于创建对象的接口,让子类决定实例化哪个具体类。工厂模式将对象的创建过程与具体类的逻辑解耦。
  • 核心思想对象的创建,由工厂负责生产特定对象。
    者手动硬编码到工厂类。在需要添加新的策略类时,Spring 容器可以帮助开发者自动注入。

对比其他模式

策略模式

  • 定义:定义一系列算法,将每种算法封装在独立的策略类中,使得这些算法可以互相替换,且算法的变化不会影响使用算法的上下文对象。
  • 核心思想动态选择算法,运行时可以替换策略对象。

模板方法模式

  • 定义:定义一个算法的框架,将算法的某些步骤延迟到子类中实现,使得子类可以在不改变算法结构的情况下重新定义算法的某些步骤。
  • 核心思想固定流程,子类负责填充或改写部分步骤。

工厂模式

  • 定义:定义一个用于创建对象的接口,让子类决定实例化哪个具体类。工厂模式将对象的创建过程与具体类的逻辑解耦。
  • 核心思想对象的创建,由工厂负责生产特定对象。

http://www.kler.cn/a/399536.html

相关文章:

  • 牛客题库 21738 牛牛与数组
  • vueRouter路由切换时实现页面子元素动画效果, 左右两侧滑入滑出效果
  • 麒麟V10,arm64,离线安装docker和docker-compose
  • LeetCode题解:18.四数之和【Python题解超详细】,三数之和 vs. 四数之和
  • web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
  • 【项目开发】理解SSL延迟:为何HTTPS比HTTP慢?
  • Java——并发工具类库线程安全问题
  • 【LeetCode热题100】字符串
  • C#编程:优化【性别和成绩名次】均衡分班
  • 一文了解Android的核心系统服务
  • 使用 Keras 训练一个卷积神经网络(CNN)(入门篇)
  • L11.【LeetCode笔记】有效的括号
  • 代码随想录算法训练营第四十七天|Day47 单调栈
  • 2022数学分析【南昌大学】
  • mini-jquery
  • Python数据分析NumPy和pandas(三十五、时间序列数据基础)
  • 炼码LintCode--数据库题库(级别:简单;数量:55道)--刷题笔记_02
  • C++【nlohmann/json】库序列化与反序列化
  • ALSA - (高级Linux声音架构)是什么?
  • ShardingSphere 如何完美驾驭分布式事务与 XA 协议?
  • HTTP常见的状态码有哪些,都代表什么意思
  • DB_redis数据一致性(三)
  • web3+web2安全/前端/钱包/合约测试思路——尝试前端绕过直接上链寻找漏洞
  • @bytemd/vue-next Markdown编辑器的使用
  • Linux下MySQL的简单使用
  • 定时器(QTimer)与随机数生成器(QRandomGenerator)的应用实践——Qt(C++)