Java 设计模式之 策略模式
为什么需要策略模式
日常工作开发中我们总会遇到如下熟悉的代码片段:
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.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个,需要使用混合模式减少类膨胀和对外暴露的问题,通过其他模式修正:工厂方法模式,代理模式,享元模式
策略模式的优缺点
一个设计模式的引入必存在它合理的地方和不足,最后我们再说说下策略模式的优缺点。
优点
- 使用策略模式,可以在不修改原有系统的基础上更换算法或行为,可以灵活地增加新的算法或行为,提供了系统的扩展性
- 策略模式提供了对一类算法进行管理维护。
- 使用策略模式可以避免使用多重条件判断,由外部模块决定所要执行的策略类。
缺点
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 会产生很多策略类,使得类的项目增多。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接