为什么需要策略模式

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

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.

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

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

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

Java 设计模式之 策略模式插图

类图中涉及三类角色: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.SimpleInstantiationStrategy 和 org.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 设计模式之 策略模式插图1

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://choupangxia.com/2020/10/22/java-strategy/