设计模式之【策略模式】

表妹:哥啊,羽绒服应该用什么模式洗呢?

:像这种比较厚的衣物,应该用标准模式洗涤,才能够洗得干净,如果是雪纺的,或者是棉麻的,这些衣物不适合长时间洗涤的话,就选择快洗模式。

表妹:这样子~

 

你看,洗衣机针对不同的衣物材料,有不同的洗衣模式。在软件开发中,我们也常常遇到这种情况,实现某一个功能有多种算法或策略,需要根据环境或者条件的不同选择不同的算法或者策略来完成该功能。比如排序、查找等。

一种常用的设计方式是硬编码(Hard Coding)在一个类中,如需要提供多种排序算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的排序算法。

也可以将这些排序算法封装在一个统一的方法中,通过if…else…或者是switch…case…等条件判断语句来进行选择。

如果要对某种排序算法进行替换,或者是新增排序算法的话,都会造成改动的范围比较大。如果可供选择的排序算法越来越多的话,就会造成该类代码比较复杂,难以维护,如果将这些策略包含到客户端,也会导致客户端代码比较庞大且难以维护。

那么,如何让算法和对象分开来,使得算法可以独立于使用它的客户而变化?

这个时候,策略模式就派上用场啦~

策略模式

定义一族算法类,将每个算法分别封装起来,让他们可以相互替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。

 

  • Strategy策略:抽象策略,是对策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。

  • Context上下文:起承上启下的作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。

  • ConcreteStrategy具体策略:用于实现抽象策略中的操作,即实现具体的算法。

假如我们需要对一个文件进行排序,文件中只包含整型数,并且相邻的数字用逗号隔开。这个文件大小的范围很大,可能只有几KB,但也可能比内存还要大,甚至可能达到TB级别。那么,就需要有不同的排序策略了,如果文件很小的话,可以放到内存中使用快排,如果超过内存大小了,那就需要进行外部排序,再大的话,还有多线程外部排序、MapReduce多机排序。

最终目的都是使文件中的所有整型数有序,但是得根据不同数量级使用不同的排序策略。这个时候,策略模式就派上用场了。

Strategy策略接口

1 public interface ISort {
2     void sort(String filePath);
3 }

ConcreteStrategy具体策略

 1 public class QuickSort implements ISort {
 2     @Override 
 3     public void sort(String filePath) {
 4         System.out.println("使用快排");
 5     }
 6 }
 7  8 public class ExternalSort implements ISort {
 9     @Override 
10     public void sort(String filePath) {
11         System.out.println("使用外部排序");
12     }
13 }
14 15 public class ConcurrentExternalSort implements ISort {
16     @Override 
17     public void sort(String filePath) {
18         System.out.println("使用多线程外部排序");
19     }
20 }
21 22 public class MapReduceSort implements ISort {
23     @Override 
24     public void sort(String filePath) {
25         System.out.println("使用MapReduce多机排序");
26     }
27 }

Context上下文角色:起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。

 1 public class Context {
 2     private ISort sortAlg;
 3     
 4     public Context(ISort sortAlg) {
 5         this.sortAlg = sortAlg;
 6     }
 7     
 8     // 这个执行策略只是通过委托调用对应的排序算法,可能没必要这个Context封装
 9     // 但是,如果排序算法比较复杂,或者存在变化,那么就需要这一层封装了。
10     public void executeStrategy(String filePath) {
11         this.sortAlg.sort();
12     }
13 }

客户端实现:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         static final long GB = 1000 * 1000 * 1000;
 4         
 5         File file = new File(filePath);
 6         long fileSize = file.length();
 7         Context executor = new Context();
 8         ISort sortAlg;
 9         if (fileSize < 6*GB) {           // [0, 6GB)
10             sortAlg = new QuickSort();
11         } else if (fileSize < 10*GB) {   // [6GB, 10GB)
12             sortAlg = new ExternalSort();
13         } else if (fileSize < 100GB) {   // [10GB, 100GB)
14             sortAlg = new ConcurrentExternalSort();
15         } else {                         // [100GB, ~)
16             sortAlg = new MapReduceSort();
17         }
18         Context executor = new Context(sortAlg);
19         executor.executeStrategy(filePath);
20     }
21 }

可能有同学会说,客户端里一堆if…else…,而且,这样子客户端必须理解所有策略算法的区别,以便选择适当的算法类。其次每种排序类都是无状态的,没必要在每次使用的时候,都重新创建一个新的对象。

是的,我们可以使用简单工厂模式来对对象的创建进行封装。

策略模式与简单工厂模式结合

策略接口和具体策略实现类不变,主要是Context中使用简单工厂模式:

 1 public class Context {
 2     static final long GB = 1000 * 1000 * 1000;
 3     private static final Map<String, ISort> algs = new HashMap<>();
 4     
 5     static {
 6         algs.put("QuickSort", new QuickSort());
 7         algs.put("ExternalSort", new ExternalSort());
 8         algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
 9         algs.put("MapReduceSort", new MapReduceSort());
10     }
11     private ISort sortAlg;
12     
13     // 使用简单工厂模式
14     public Context(long fileSize) {
15         if (fileSize < 6*GB) {           // [0, 6GB)
16             this.sortAlg = algs("QuickSort");
17         } else if (fileSize < 10*GB) {   // [6GB, 10GB)
18             this.sortAlg = algs("ExternalSort");
19         } else if (fileSize < 100GB) {   // [10GB, 100GB)
20             this.sortAlg = algs("ConcurrentExternalSort");
21         } else {                         // [100GB, ~)
22             this.sortAlg = algs("MapReduceSort");
23         }
24     }
25     
26     public void executeStrategy(String filePath) {
27         this.sortAlg.sort();
28     }
29 }

客户端实现:

1 public class Demo {
2     public static void main(String[] args) {   
3         File file = new File(filePath);
4         long fileSize = file.length();
5         Context executor = new Context(fileSize);
6         executor.executeStrategy(filePath);
7     }
8 }

将实例化具体策略的过程由客户端转移到Context类中,你看,这样的客户端代码是不是简洁多了,如果有新的排序算法,只需要修改Context类即可,而且实现客户端与ConcreteStrategy解耦。

策略模式的优点

  • 算法可以自由切换

    这是策略模式本身定义的,只要实现抽象策略,它就成为策略家族的一个成员,通过封装角色对其进行封装,保证对外提供“可以自由切换”的策略。

  • 避免使用多重条件判断

  • 扩展性好

    在现有的系统中增加一个策略很容易,只要实现接口就可以了。

  • 策略模式把算法的使用放到Context类中,而算法的实现移到具体的策略类中,实现二者的分类。

策略模式的缺点

  • 每个策略都是一个类,复用的可能性很小,类数量增多。

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。此时可能不得不向客户暴露具体的实现逻辑,因此,通常会与工厂方法结合使用。

策略模式的应用场景

  • 许多相关的类仅仅是行为不同的时候,使用策略模式,提供了一种用多个行为中的一个行为来配置一个类的方法。即一个系统需要动态地在几种算法中选择一种。

  • 需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时,可以使用策略模式。

  • 算法使用客户不应该知道的数据,可使用策略模式以避免暴露复杂的、与算法相关的数据结构。

  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。

策略模式在开源代码中的应用

我们知道,Spring AOP是通过动态代理来实现的,Spring支持两种动态代理实现方式,一种是JDK提供的动态代理实现方式,另一种是Cglib提供的动态代理实现方式。

Spring会在运行时动态地选择不同的动态代理实现方式,这个应用场景实际上就是策略模式的典型应用场景。

AopProxy是策略接口,定义如下:

JdkDynamicAopProxy、CglibAopProxy是两个实现了AopProxy接口的策略类。

在策略模式中,策略的创建一般通过工厂方法来实现。对应到Spring源码,AopProxyFactory是一个工厂类接口,DefaultAopProxyFactory是一个默认的工厂类,用来创建AopProxy对象。

总结

封装不同的算法,算法之间能互相替换。

参考

极客时间专栏《设计模式之美》

Java设计模式之策略模式炸斯特的博客-CSDN博客设计模式之策略模式