该图片由daschorsch在Pixabay上发布
你好,我是看山。
本文收录在《一个架构师的职业素养》专栏,日拱一卒,功不唐捐。
定义
策略模式,英文全称是 Strategy Design Pattern。在 GoF 的《设计模式》一书中,它是这样定义的:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端。
这里所说的客户端代指使用算法的代码。
根据使用场景分类,策略模式是一种行为型模式,用于运行时控制类的行为或算法。
使用上的直观感受是,策略模式可以减少了 if-else/switch 分支代码。那减少分支代码有什么好处呢?
解耦代码,策略模式就是解耦不同算法实现;
减少 bug 产生概率,减少分支,就是减少 bug 发生概率。
有编程经验的都知道,很多 bug 都是从分支逻辑产生的。我刚开始工作时晚上 12 点开始抓虫,一直抓到凌晨 2 点多,最后发现是有一个 if-else 分支中,在某个 if 前面少写了一个 else。下面是示例,实际代码比这个复杂很多:
if (a < 1) { } else if (a < 2) { } else if (a < 3) { }
结果写成了:
if (a < 1) { } else if (a < 2) { } if (a < 3) { }
代码编译不会错,但是在执行时,某些 case 会不符合预期。
问题
我们来看看策略模式出现的场景。
以电商系统的支付功能为例,最早的时候,我们可能为了更快上线,选择一个较多人使用的支付方式,比如微信支付(也有可能是支付宝支付,根据售卖场景不同区分)。这个时候,我们只需要判断用户是从 PC 页面进入还是 H5 进入即可。
后来,业务发展比较好,涉及人群更多了,于是需要对接支付宝支付。支付宝支付也分为了多种的支付场景,对接接口变多了,但是也在可控范围内。
再后来,我们需要对接银联支付、对接各银行接口,等等,支付接口变得越来越臃肿。于是,每对接一种支付方式,支付相关接口就会增加一倍。此时,这坨臃肿的代码,无论是修复简单的 bug,还是微调传输参数,都会影响整个支付逻辑,从而增加了在已有正常运行代码中引入错误的风险。
如果是多人协作开发,我们还会陷入代码合并时应付各种冲突的情况。终于,在某一时刻,我们看着这一坨代码,已经无从下手维护了。
解决方案
首先,我们来分析一下上面的场景,不变的是系统内部的支付业务逻辑,变化的是支付方式。
支付方式的可变性在于,可能会与多种支付方式对接,对接参数、协议、地址等都会不同。根据设计模式的整体思想,我们将变化的单独出去,将不变的稳定下来。
这种处理方式就是策略模式建议的:找出负责用许多不同方式完成特定任务的类,然后将其中的算法抽取到一族被称为策略的独立类中。
调用这些策略类的是调用上下文,它持有对所有策略类的引用。上下文不执行任务,它是任务的指挥者,将工作委派给已连接的策略对象。关系如下:
很多教程到这里就结束了,如果你能够看到这里,而且还用心看了,你就会发现一丝丝的不一样。
根据迪米特法则(LOD,Law of Demeter),上下文不需要知道具体策略类的功能,只需要通过特定的接口,用于触发选中策略即可。也就是说,完整的策略模式,应该有具体的策略判断是否由该策略执行,上下文只需要知道有哪些策略就行了。这样改动之后,上下文还能够与工厂模式结合。如果策略是无状态策略,还可以在上下文中引入单例模式。
适用与不适用
根据上面的定义,策略模式是围绕可以互换的算法来创建业务的。简单的说就是,分支逻辑隔离。
当你想使用各种不同算法变体,且能够在运行时切换算法。策略模式可以将对象关联到不同实现方式的不同子任务中,可以间接的修改对象;
只有在执行时有些许不同的相似算法。可以将不同的行为抽象到独立的类中,在原来的类中调用这些独立的算法;
算法在业务逻辑中不是特别重要。我们可以通过策略模式将算法、数据、依赖等抽离出来,在运行时调用即可。
设计模式只是解决问题的优雅实现,并不一定适用所有情况,比如下面这几种,就可以不用非得实现策略模式:
如果算法极少变化,就没有任何理由引入新的类和接口。
如果使用了 Java8 之后的版本,可以使用函数式编程,有时候就使用 Lambda 表达式或者匿名内部类的方式实现具体算法即可。
示例代码
还是以支付为例,因为都是演示,一切从简。我曾经主导过支付中台,如果想要具体实现,可以具体聊一下。
首先定义支付策略接口:
public interface PayStrategy { String payType(); void callPay(BigDecimal amount); }
payType()是在具体的策略实现中定义策略可执行的支付方式,也可以通过传参数的方式返回boolean类型用于判断是否可执行。
然后是微信支付和支付宝支付分别实现支付策略接口:
public class WxpayPayStrategy implements PayStrategy { @Override public String payType() { return "WXPAY"; } @Override public void callPay(BigDecimal amount) { // 微信支付接口 // 这里只是演示,即使都是微信支付,也会分不同的接口 System.out.println("调用微信支付接口"); } } public class AlipayPayStrategy implements PayStrategy { @Override public String payType() { return "ALIPAY"; } @Override public void callPay(BigDecimal amount) { // 调用支付宝支付接口 // 这里只是演示,即使都是支付宝支付,也会分不同的接口 System.out.println("调用支付宝支付接口"); } }
我们再来看看持有策略算法的上下文:
public class StrategyContext { private static final Map<String, PayStrategy> PAY_STRATEGY_MAP = new HashMap<>(); static { final AlipayPayStrategy alipayPayStrategy = new AlipayPayStrategy(); final WxpayPayStrategy wxpayPayStrategy = new WxpayPayStrategy(); PAY_STRATEGY_MAP.put(alipayPayStrategy.payType(), alipayPayStrategy); PAY_STRATEGY_MAP.put(wxpayPayStrategy.payType(), wxpayPayStrategy); } public void pay(String payType, BigDecimal amount) { final PayStrategy payStrategy = PAY_STRATEGY_MAP.get(payType); payStrategy.callPay(amount); } }
可以看到,上下文只需要知道策略算法的存在,至于算法是否符合要求,由算法自己判断。
调用就比较简单了:
public class Main { public static void main(String[] args) { final StrategyContext strategyContext = new StrategyContext(); strategyContext.pay("ALIPAY", BigDecimal.TEN); strategyContext.pay("WXPAY", BigDecimal.ONE); } }
文末总结
策略模式可能用来减少分支逻辑,将不同的算法分离开来。如果配合工厂模式、单例模式,可以更加灵活的使用。如果是在 Spring 当中,借助自动注入,上下文甚至可以不知道具体策略实现。
最近刚看到一句话,“日拱一卒,功不唐捐”。坚持下去,每天学点新东西,给生活加点色彩。
推荐阅读
Java 中的单例模式(完整篇)
设计模式:建造者模式