掌握设计模式之策略模式

前言

最近段时间,接到一个需求:开发一个聚合支付服务,对其他内部项目提供统一的接口来实现不同支付平台的支付能力发起,比如支付宝,微信,银联等。为了处理相似的支付操作而各平台具体实现不同的情况,要让各个平台接口能力能相互独立,并要方便扩展后续新增的支付平台,我引入了设计模式的策略模式来应对需求场景,借此深入学习总结下策略模式,于是也就有了本文,希望对学习策略模式的同学有所帮助。

为什么需要策略模式

日常工作开发中我们总会遇到如下熟悉的代码片段:

if(condition1){
 // do something1
} else if (condition2){
 // do something2
} else if (condition3){
 // do something3
}

在每个 if 条件下都有数十行甚至百行的业务处理,各自处理又是相互独立的并且目的一致,都汇聚在一个方法里。这样的写法不但让类变得臃肿冗长,并且不同逻辑都在一个类中修改,维护和扩展起来都很费劲。那么又有什么办法可以优化这大段的代码呢,在实现功能的同时,让代码更加灵活和易维护。

要解决这个问题,本文的主角—策略模式 就登场了,作为设计模式中比较简单的行为型模式,其实很多框架中都见到它的身影,稍后我们也会从各框架源码中识别策略模式的应用。使用策略模式可以帮助我们将每个处理逻辑封装成独立的类,客户端类需要进行哪种处理逻辑就使用对应的类,调用其封装了业务处理细节的方法即可。这样一来,客户端类减少了业务处理逻辑的大量代码,让自身更加精简。当业务逻辑有所改动时,只要在对应的类中修改,而不影响其他的类;并且如果出现了新的业务逻辑只要新增相似的类进行实现,供客户端类调用即可。

什么是策略模式

接下来我们就介绍下策略模式的定义和组成,以及它的基本形式。

首先看下维基百科上策略模式的定义:

In computer programming, the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.

策略模式也叫政策模式,允许程序在运行时选择一个算法执行,通常存在一类算法实现提供外部选择执行,这里的算法,也可以叫做策略,相当于上节内容提到的具体处理逻辑。

再来看下 《设计模式:可复用面向对象软件的基础》一书中对策略模式的定义:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.

再次对其定义解读:定义一类算法,各自独立封装实现,并且相互之间是可替换的。除此之外,由客户端类决定具体使用哪个算法。

上述两个定义都提到了算法一词,它表示了完整的,不可再拆分的业务逻辑处理。通常用接口或者抽象类来表示一类算法的抽象,提供多种对该类算法的操作实现,以此组成一类独立且可替换的算法,也叫策略组。

了解完定义后,我们再来看下策略模式通用类图:

类图中涉及三类角色:Context,Strategy 和 ConcreteStrategy

  • Strategy:抽象策略角色,代表某个算法的接口或者抽象类,定义了每个算法或者策略需要具有的方法和属性。
  • Context:上下文角色,引用策略接口对象,屏蔽了外部模块对策略或方法的直接访问,只能通过Context 提供的方法访问。
  • ConcreteStrategy:抽象策略的具体实现,该类含有具体的算法,并且通常不只一种实现,有多个类。

这三个角色的功能职责都十分明确,对应的源码实现也十分简单,现在我们就来快速看下每个角色对应的通用源码。

// 抽象的策略角色
public interface Strategy {
    void doSomething();
}

// 具体策略角色
public class ConcreteStrategy implements Strategy {
    @Override
    public void doSomething() {
        System.out.println("ConcreteStrategy doSomething !");
    }
}

// 上下文角色
public class Context {
    private final Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void doAnything() {
        this.strategy.doSomething();
    }
}

有了策略模式的基本代码结构,在客户端类中使用十分简单,想要哪个策略,就产生出它的具体策略对象放入上下文对象内,然后由上下文对象执行具体策略操作即可,具体代码如下:

public class Client {
    public static void main(String[] args) {
        Strategy strategy = new ConcreteStrategy();
        Context context = new Context(strategy);
        context.doAnything(); // ConcreteStrategy doSomething !
    }
}

识别策略模式

看清楚了策略模式的定义,角色组成以及通用的代码结构之后,我们就来看下策略模式在通用框架里的应用,来加深对策略模式的认识。

JDK 与策略模式

在常用的Java 集合框架中,比较器 java.util.Comparator 的设计就采用了策略模式。Comparator 就是一个抽象的策略接口,只要一个类实现这个接口,自定 compare 方法,该类成为具体策略类,你可以在很多地址找到这个抽象策略接口的实现,官方在工具类 java.util.Comparators 里也提供 NaturalOrderComparator,NullComparator 两种具体策略类。而使用 Comparator 到的 java.util.Collections 类就是 Context 角色,将集合的比较功能封装成静态方法对外提供。

Spring Framework 与策略模式

Spring 框架最早以 IoC 和 DI 两大特性著称,不需要开发者自己创建对象,而是通过 Spring IoC 容器识别然后实例化所需对象。在 Spring 中将执行创建对象实例的这个操作封装为一种算法,用接口类 org.springframework.beans.factory.support.InstantiationStrategy 进行声明,而具体策略类则有 org.springframework.beans.factory.support.SimpleInstantiationStrategyorg.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy 两个,并且 CglibSubclassingInstantiationStrategy 是对 SimpleInstantiationStrategy 的继承扩展,也是 Spring 容器中真正使用到的策略类,具体应用的源码可参考 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory 类:

/**
    * Instantiate the given bean using its default constructor.
    * @param beanName the name of the bean
    * @param mbd the bean definition for the bean
    * @return a BeanWrapper for the new instance
    */
protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
    //...
    beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
    //...
}

如何使用策略模式

实例应用

俗话说学以致用,接触了策略模式后我们应该想想怎么用在自己日常开发项目中呢,这里就简单通过一个实例来说明下策略模式的使用方式。假设现在有个需求:需要对一个目录或者文件实现两种不同格式的解压缩方式:zip压缩和gzip压缩,也后续可能新增其他的解压缩方式。

我们首先将解压缩的算法抽象成抽象策略接口 CompressStrategy, 提供压缩方法 compress 和解压缩方法 uncompress,分别接受源文件路径和目的文件路径。

策略类在命名通常上以 Strategy 为后缀,来指明自身采用策略模式进行设计,以此简化与其他人沟通成本。

public interface CompressStrategy {
    public boolean compress(String source, String to);
    public boolean uncompress(String source, String to);
}

再对抽象策略接口进行实现,分别提供zip 压缩算法和 gzip 压缩算法,代码如下:

public class ZipStrategy implements CompressStrategy {

    @Override
    public boolean compress(String source, String to) {
        System.out.println(source + " --> " + to + " ZIP压缩成功!");
        return true;
    }

    @Override
    public boolean uncompress(String source, String to) {
        System.out.println(source + " --> " + to + " ZIP解压缩成功!");
        return true;
    }
}

public class GzipStrategy implements CompressStrategy {

    @Override
    public boolean compress(String source, String to) {
        System.out.println(source + " --> " + to + " GZIP压缩成功!");
        return true;
    }

    @Override
    public boolean uncompress(String source, String to) {
        System.out.println(source + " --> " + to + " GZIP解压缩成功!");
        return true;
    }
}

代码示例里的实现为了简化只是简单打印操作,具体实现可以参考 JDK API 进行操作。

接下来看下 Context 角色的代码实现:

public class CompressContext {

    private CompressStrategy compressStrategy;

    public CompressContext(CompressStrategy compressStrategy) {
        this.compressStrategy = compressStrategy;
    }

    public boolean compress(String source, String to) {
        return compressStrategy.compress(source, to);
    }

    public boolean uncompress(String source, String to) {
        return compressStrategy.uncompress(source, to);
    }
}

十分简单,只是传入一个具体算法,然后执行,到这里标准的策略模式就编写完毕了。客户端类只是根据需要指定的具体压缩策略对象传给 CompressContext 对象即可。如果要新增一个压缩算法,也只需对 CompressStrategy 接口提供新的实现即可传给 CompressContext 对象使用。

public class Client {
    public static void main(String[] args) {
        CompressContext context;
        System.out.println("========执行算法========");
        context = new CompressContext(new ZipStrategy());
        context.compress("c:\\file", "d:\\file.zip");
        context.uncompress("c:\\file.zip", "d:\\file");
        System.out.println("========切换算法========");
        context = new CompressContext(new GzipStrategy());
        context.compress("c:\\file", "d:\\file.gzip");
        context.uncompress("c:\\file.gzip", "d:\\file");
    }
}

上面的策略模式的应用示例是不是很简单,类似应用也有很多,比如要对接第三方支付,不同的支付平台有不同的支付API,这个API操作都可以抽象成策略接口,客户端发起特定平台的支付接口时,我们只需调用具体的支付策略类执行,并且每个支付策略类相互独立,可替换。

适用场景

本节最后简单总结下策略模式的适用场景:

  • 如果一个对象有很多的行为,它们的实现目的相同,而这些行为使用了多重的条件选择语句来实现。

  • 当一个系统需要动态地切换算法,会选择一种算法去执行。

  • 客户端类不需要知道具体算法的实现细节,只要调用并完成所需要求。

Lambda 与 策略模式

JDK 8 之后,利用Lambda可以提供策略模式更加精简的实现,如果策略接口是一个函数接口,那么不需要声明新的类来实现不同策略,直接通过传递Lambda就可实现,并且更加简洁,具体使用方式参见下方代码:

/**
 * Context 对象
 */
public class Validator {
    private final ValidationStrategy strategy;

    public Validator(ValidationStrategy v) {
        this.strategy = v;
    }

    public boolean validate(String s) {
        return strategy.execute(s);
    }

}

/**
 * 策略接口
 */
@FunctionalInterface
public interface ValidationStrategy {
    boolean execute(String s);
}

numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
b1 = numericValidator.validate("aaaa"); // true
lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
b2 = lowerCaseValidator.validate("bbbb"); // false

结合 Lambda 的策略模式更适合用于处理简单算法操作的场景,如果算法实现复杂过于冗长复杂,还是建议拆分成单个类进行实现。

策略模式的注意点

策略模式使用起来虽然简单,但它的灵活性在许多项目都能见到其身影,在使用时也有需要注意的地方,下面我们就来看下:

  • 策略模式中每个算法都是完整,不可拆分的原子业务,并且多个算法必须是可以相互替换,,而用哪个算法由外部调用者决定。

  • 当如果具体策略类超过4个,需要使用混合模式减少类膨胀和对外暴露的问题,通过其他模式修正:工厂方法模式,代理模式,享元模式

策略模式的优缺点

一个设计模式的引入必存在它合理的地方和不足,最后我们再说说下策略模式的优缺点。

优点

  • 使用策略模式,可以在不修改原有系统的基础上更换算法或行为,可以灵活地增加新的算法或行为,提供了系统的扩展性
  • 策略模式提供了对一类算法进行管理维护。
  • 使用策略模式可以避免使用多重条件判断,由外部模块决定所要执行的策略类。

缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  • 会产生很多策略类,使得类的项目增多。

结语

到这里,本文对策略模式的学习就此结束,当然关于策略模式的内容远不止这些,配合其他模式还有用法,感兴趣了的同学可以参考文末提供的资料链接进一步深入学习。也欢迎扫码关注微信公众号:「闻人的技术博客」,定期分享Java技术干货,共同进步。

推荐阅读

参考

本文由博客一文多发平台 OpenWrite 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • javascript设计模式与开发实践 设计模式 每个设计模式我们需要从三点问题入手: 定义 作用 用法与实现 单...
    穿牛仔裤的蚊子阅读 4,045评论 0 13
  • 0.提前说明 模式选择的方法1)模式的功能——看是否能解决问题2)模式的本质——看模式是否主要用来解决这类问题3)...
    王侦阅读 1,043评论 0 1
  • 1.初识策略模式 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户...
    王侦阅读 1,450评论 0 3
  • 生命之旅总是那么漫长, 总要经历无数个披荆斩刺, 漫长的来来往往的旅程中, 记住的和遗忘的很多, ...
    400b0e23cdf8阅读 153评论 0 2
  • 四年前,离开西南去关中求学,由于各种事放假回家时间极短,四年时间过去,同班的同学已早毕业或者考研,而作为医学生的我...
    QT不息的风阅读 557评论 0 0