14.策略者模式设计思想
目录介绍
- 01.策略模式基础介绍
- 1.1 策略模式由来
- 1.2 策略模式定义
- 1.3 策略模式场景
- 1.4 策略模式思考
- 02.策略模式原理与实现
- 2.1 罗列一个场景
- 2.2 用一个例子理解策略
- 2.3 案例演变分析
- 2.4 策略模式官方实现
- 03.策略模式结构
- 3.1 策略模式结构图
- 3.2 策略模式时序图
- 04.策略模式案例
- 4.1 业务需求分析
- 4.2 业务实现
- 4.3 用策略模式实现
- 4.4 还有优化空间吗
- 05.策略者优缺点
- 5.1 策略模式的优点
- 5.2 策略模式的缺点
- 06.策略模式拓展
- 6.1 策略VS状态模式
01.策略模式基础介绍
1.1 策略模式由来
- 完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。
- 有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中。
- 如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,
- 通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。
- 在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。
- 为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法。
- 在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应于一个具体策略类。
1.2 策略模式定义
- 策略模式(Strategy Pattern):
- 定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。
- 策略模式属于对象的行为模式。
- 其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。
1.3 策略模式场景
- 在以下情况下可以使用策略模式:
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
- 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法和相关的数据结构,提高算法的保密性与安全性。
1.4 策略模式思考
- 策略模式主要是解决避免 if-else 分支判断逻辑的吗?
- 实际上,这种认识是很片面的。策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。
- 除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入 bug 的风险。
- 策略模式是否支持多态的使用?
- 通过让环境类持有一个抽象策略类(超类)的引用,在生成环境类实例对象时,让该引用指向具体的策略子类。再对应的方法调用中,就会通过Java的多态,调用对应策略子类的方法。
- 从而可以相互替换,不需要修改环境类内部的实现。同时,在有新的需求的情况下,也只需要修改策略类即可,降低与环境类之间的耦合度。
- 策略模式的重心是什么?
- 策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。
- 运行时策略的唯一性
- 运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。
02.策略模式原理与实现
2.1 罗列一个场景
- 假设现在要设计一个贩卖各类书籍的电子商务网站的购物车系统。
- 一个最简单的情况就是把所有货品的单价乘上数量,但是实际情况肯定比这要复杂。
- 比如,本网站可能对所有的高级会员提供每本20%的促销折扣;对中级会员提供每本10%的促销折扣;对初级会员没有折扣。
- 根据描述,折扣是根据以下的几个算法中的一个进行的:
- 算法一:对初级会员没有折扣。
- 算法二:对中级会员提供10%的促销折扣。
- 算法三:对高级会员提供20%的促销折扣。
- 算法四:对钻石会员提供30%的促销折扣。
- 看下面代码,利用面向对象设计原则如何改进,可以保证代码后期的拓展性和解耦问题?
- 将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展?
- 看看下面这段代码,改编某伟大公司产品代码,你觉得可以利用面向对象设计原则如何改进?
//将策略的定义、创建、使用直接耦合在一起。 private void calcPrice(int type,double booksPrice) { double price; if (type == 1) { price = booksPrice * 0.9; System.out.println("对中级会员提供10%的促销折扣"); } else if (type == 2) { price = booksPrice * 0.8; System.out.println("对高级会员提供20%的促销折扣"); } else if (type == 3) { price = booksPrice * 0.7; System.out.println("对钻石会员提供30%的促销折扣"); } else { System.out.println("对初级会员没有折扣"); price = booksPrice; } System.out.println("图书的最终价格为:" + price); }
- 问题分析一下,如果再多增加几种优惠类型,那么业务就更加复杂了。
2.2 用一个例子理解策略
- 使用策略模式来实现的结构图如下:
- 代码案例如下所示
- 利用开原则,可以尝试改造为下面的代码。将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展。
- 抽象折扣类。这个相当于抽象策略
public interface MemberStrategy { /** * 计算图书的价格 * @param booksPrice 图书的原价 * @return 计算出打折后的价格 */ public double calcPrice(double booksPrice); }
- 初级会员折扣类。这个是具体策略1
public class PrimaryMemberStrategy implements MemberStrategy { @Override public double calcPrice(double booksPrice) { System.out.println("对于初级会员的没有折扣"); return booksPrice; } }
- 中级会员折扣类。这个是具体策略2
public class IntermediateMemberStrategy implements MemberStrategy { @Override public double calcPrice(double booksPrice) { System.out.println("对于中级会员的折扣为10%"); return booksPrice * 0.9; } }
- 高级会员折扣类。这个是具体策略3
public class AdvancedMemberStrategy implements MemberStrategy { @Override public double calcPrice(double booksPrice) { System.out.println("对于高级会员的折扣为20%"); return booksPrice * 0.8; } }
- 价格类。这个相当于环境角色
public class Price { //持有一个具体的策略对象 private MemberStrategy strategy; /** * 构造函数,传入一个具体的策略对象 * @param strategy 具体的策略对象 */ public Price(MemberStrategy strategy){ this.strategy = strategy; } /** * 计算图书的价格 * @param booksPrice 图书的原价 * @return 计算出打折后的价格 */ public double quote(double booksPrice){ return this.strategy.calcPrice(booksPrice); } }
- 具体调用:
public static void main(String[] args) { //选择并创建需要使用的策略对象 MemberStrategy strategy = new AdvancedMemberStrategy(); //创建环境 Price price = new Price(strategy); //计算价格 double quote = price.quote(300); System.out.println("图书的最终价格为:" + quote); }
2.3 案例演变分析
2.4 策略模式官方实现
- 策略模式官方实现案例如下所示
public interface Strategy{ void algorithm(); } public static class ConcreteStrategyA implements Strategy { @Override public void algorithm() { System.out.println("use algorithm A"); } } public static class ConcreteStrategyB implements Strategy { @Override public void algorithm() { System.out.println("use algorithm B"); } } public static class Context { private final Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public void algorithm() { strategy.algorithm(); } }
03.策略模式结构
3.1 策略模式结构图
- 策略模式包含如下角色:
- 环境(Context)角色:持有一个Strategy的引用,即具有复杂多变行为的对象。
- 抽象策略(Strategy)角色:抽象策略类,这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(ConcreteStrategy)角色:具体策略类,包装了相关的算法或行为。一般实际场景中会有多个
- 策略模式结构图
image
3.2 策略模式时序图
- 策略模式时序图
image
04.策略模式案例
4.1 业务需求分析
- 你可能会说,这不是很简单嘛,只需要将文件中的内容读取出来,并且通过逗号分割成一个一个的数字,放到内存数组中,然后编写某种排序算法(比如快排),或者直接使用编程语言提供的排序函数,对数组进行排序,最后再将数组中的数据写入文件就可以了。
- 但是,如果文件很大呢?比如有 10GB 大小,因为内存有限(比如只有 8GB 大小),我们没办法一次性加载文件中的所有数据到内存中,这个时候,我们就要利用外部排序算法。
- 如果文件更大,比如有100GB大小,我们为了利用CPU多核的优势,可以在外部排序的基础之上进行优化,加入多线程并发排序的功能,这就有点类似“单机版”的 MapReduce。
- 如果文件非常大,比如有1TB大小,即便是单机多线程排序,这也算很慢了。这个时候,我们可以使用真正的 MapReduce 框架,利用多机的处理能力,提高排序的效率。
4.2 业务实现
- 用最简单直接的方式将它实现出来。
- 具体代码我贴在下面了,你可以先看一下。因为我们是在讲设计模式,不是讲算法,所以,在下面的代码实现中,我只给出了跟设计模式相关的骨架代码,并没有给出每种排序算法的具体代码实现。
public static class Sorter { private static final long GB = 1000 * 1000 * 1000; public void sortFile(String filePath) { // 省略校验逻辑 File file = new File(filePath); long fileSize = file.length(); if (fileSize < 6 * GB) { // [0, 6GB) quickSort(filePath); } else if (fileSize < 10 * GB) { // [6GB, 10GB) externalSort(filePath); } else if (fileSize < 100 * GB) { // [10GB, 100GB) concurrentExternalSort(filePath); } else { // [100GB, ~) mapreduceSort(filePath); } } private void quickSort(String filePath) { // 快速排序 } private void externalSort(String filePath) { // 外部排序 } private void concurrentExternalSort(String filePath) { // 多线程外部排序 } private void mapreduceSort(String filePath) { // 利用MapReduce多机排序 } }
- 在“编码规范”那一部分我们讲过,函数的行数不能过多,最好不要超过一屏的大小。
- 所以,为了避免 sortFile() 函数过长,我们把每种排序算法从 sortFile() 函数中抽离出来,拆分成 4 个独立的排序函数。
- 如果只是开发一个简单的工具,那上面的代码实现就足够了。毕竟,代码不多,后续修改、扩展的需求也不多,怎么写都不会导致代码不可维护。
- 但是,如果我们是在开发一个大型项目,排序文件只是其中的一个功能模块,那我们就要在代码设计、代码质量上下点儿功夫了。只有每个小的功能模块都写好,整个项目的代码才能不差。
- 实际上,如果自己实现一下的话,你会发现,每种排序算法的实现逻辑都比较复杂,代码行数都比较多。所有排序算法的代码实现都堆在 Sorter 一个类中,这就会导致这个类的代码很多。
- 而在“编码规范”那一部分中,我们也讲到,一个类的代码太多也会影响到可读性、可维护性。除此之外,所有的排序算法都设计成 Sorter 的私有函数,也会影响代码的可复用性。
4.3 用策略模式实现
- 针对上面的问题,即便我们想不到该用什么设计模式来重构,也应该能知道该如何解决,那就是将 Sorter 类中的某些代码拆分出来,独立成职责更加单一的小类。
- 实际上,拆分是应对类或者函数代码过多、应对代码复杂性的一个常用手段。按照这个解决思路,我们对代码进行重构。重构之后的代码如下所示:
public class SorterDesign { private static final long GB = 1000 * 1000 * 1000; public void sortFile(String filePath) { // 省略校验逻辑 File file = new File(filePath); long fileSize = file.length(); ISortAlg sortAlg; if (fileSize < 6 * GB) { // [0, 6GB) sortAlg = new QuickSort(); } else if (fileSize < 10 * GB) { // [6GB, 10GB) sortAlg = new ExternalSort(); } else if (fileSize < 100 * GB) { // [10GB, 100GB) sortAlg = new ConcurrentExternalSort(); } else { // [100GB, ~) sortAlg = new MapReduceSort(); } sortAlg.sort(filePath); } } public interface ISortAlg { void sort(String filePath); } public class QuickSort implements ISortAlg { @Override public void sort(String filePath) { //... } } public class ExternalSort implements ISortAlg { @Override public void sort(String filePath) { //... } } public class ConcurrentExternalSort implements ISortAlg { @Override public void sort(String filePath) { //... } } public class MapReduceSort implements ISortAlg { @Override public void sort(String filePath) { //... } }
- 经过拆分之后,每个类的代码都不会太多,每个类的逻辑都不会太复杂,代码的可读性、可维护性提高了。
- 除此之外,我们将排序算法设计成独立的类,跟具体的业务逻辑(代码中的 if-else 那部分逻辑)解耦,也让排序算法能够复用。
- 这一步实际上就是策略模式的第一步,也就是将策略的定义分离出来。
4.4 还有优化空间吗
- 实际上,上面的代码还可以继续优化。
- 每种排序类都是无状态的,我们没必要在每次使用的时候,都重新创建一个新的对象。所以,我们可以使用工厂模式对对象的创建进行封装。
- 按照这个思路,我们对代码进行重构。重构之后的代码如下所示:
public class SortAlgFactory { private static final Map<String, ISortAlg> algs = new HashMap<>(); static { algs.put("QuickSort", new QuickSort()); algs.put("ExternalSort", new ExternalSort()); algs.put("ConcurrentExternalSort", new ConcurrentExternalSort()); algs.put("MapReduceSort", new MapReduceSort()); } public static ISortAlg getSortAlg(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } return algs.get(type); } } public class Sorter3 { private static final long GB = 1000 * 1000 * 1000; public void sortFile(String filePath) { // 省略校验逻辑 File file = new File(filePath); long fileSize = file.length(); ISortAlg sortAlg; if (fileSize < 6 * GB) { // [0, 6GB) sortAlg = SortAlgFactory.getSortAlg("QuickSort"); } else if (fileSize < 10 * GB) { // [6GB, 10GB) sortAlg = SortAlgFactory.getSortAlg("ExternalSort"); } else if (fileSize < 100 * GB) { // [10GB, 100GB) sortAlg = SortAlgFactory.getSortAlg("ConcurrentExternalSort"); } else { // [100GB, ~) sortAlg = SortAlgFactory.getSortAlg("MapReduceSort"); } sortAlg.sort(filePath); } }
- 经过上面两次重构之后,现在的代码实际上已经符合策略模式的代码结构了。
- 我们通过策略模式将策略的定义、创建、使用解耦,让每一部分都不至于太复杂。
- 不过,Sorter 类中的 sortFile() 函数还是有一堆 if-else 逻辑。这里的 if-else 逻辑分支不多、也不复杂,这样写完全没问题。
- 但如果你特别想将 if-else 分支判断移除掉,那也是有办法的。我直接给出代码,你一看就能明白。实际上,这也是基于查表法来解决的,其中的“algs”就是“表”。
public class Sorter4 { private static final long GB = 1000 * 1000 * 1000; private static final List<AlgRange> algs = new ArrayList<>(); static { algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort"))); algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort"))); algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort"))); algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort"))); } public void sortFile(String filePath) { // 省略校验逻辑 File file = new File(filePath); long fileSize = file.length(); ISortAlg sortAlg = null; for (AlgRange algRange : algs) { if (algRange.inRange(fileSize)) { sortAlg = algRange.getAlg(); break; } } sortAlg.sort(filePath); } private static class AlgRange { private long start; private long end; private ISortAlg alg; public AlgRange(long start, long end, ISortAlg alg) { this.start = start; this.end = end; this.alg = alg; } public ISortAlg getAlg() { return alg; } public boolean inRange(long size) { return size >= start && size < end; } } }
- 现在的代码实现就更加优美了。我们把可变的部分隔离到了策略工厂类和 Sorter 类中的静态代码段中。当要添加一个新的排序算法时,我们只需要修改策略工厂类和 Sort 类中的静态代码段,其他代码都不需要修改,这样就将代码改动最小化、集中化了。
- 你可能会说,即便这样,当我们添加新的排序算法的时候,还是需要修改代码,并不完全符合开闭原则。有什么办法让我们完全满足开闭原则呢?
- 对于 Java 语言来说,我们可以通过反射来避免对策略工厂类的修改。具体是这么做的:我们通过一个配置文件或者自定义的 annotation 来标注都有哪些策略类;策略工厂类读取配置文件或者搜索被 annotation 标注的策略类,然后通过反射动态地加载这些策略类、创建策略对象;当我们新添加一个策略的时候,只需要将这个新添加的策略类添加到配置文件或者用 annotation 标注即可。还记得上一节课的课堂讨论题吗?我们也可以用这种方法来解决。
- 对于 Sorter 来说,我们可以通过同样的方法来避免修改。我们通过将文件大小区间和算法之间的对应关系放到配置文件中。当添加新的排序算法时,我们只需要改动配置文件即可,不需要改动代码。
05.策略者优缺点
5.1 策略模式的优点
- 策略模式的优点
- 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
- 策略模式提供了管理相关的算法族的办法。
- 策略模式提供了可以替换继承关系的办法。
- 使用策略模式可以避免使用多重条件转移语句。
5.2 策略模式的缺点
- 策略模式的缺点
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
参考
- 策略模式
- https://design-patterns.readthedocs.io/zh-cn/latest/behavioral_patterns/strategy.html