人人都会设计模式:策略模式

人人都会设计模式:策略模式

该图片由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 中的单例模式(完整篇)

设计模式:建造者模式

上一篇:使用主机ip地址绑定GooglAppEngine站点


下一篇:《树莓派渗透测试实战》——1.1 购买树莓派