--- title: 设计模式-05.02-行为型-策略&职责链 tags: - 策略模式 - 职责链模式 categories: - 设计模式 - 05.行为型 keywords: 策略模式,职责链模式 description: 不多说,看文章 cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' abbrlink: 2c3cc5fd date: 2021-08-01 15:51:58 --- # 策略模式【常用】 策略模式。在实际的项目开发中,这个模式也比较常用。最常见的应用场景是,利用它来避免冗长的 if-else 或 switch 分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点等等。 ## 策略模式的原理与实现 1. 策略模式,英文全称是 Strategy Design Pattern。在 GoF 的《设计模式》一书中,它是这样定义的: > Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. 2. 翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。 3. 我们知道,工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式跟两者类似,也能起到解耦的作用,不过,它解耦的是策略的定义、创建、使用这三部分。接下来,我就详细讲讲一个完整的策略模式应该包含的这三个部分。 ### 策略的定义 策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。示例代码如下所示: ```java public interface Strategy { void algorithmInterface(); } public class ConcreteStrategyA implements Strategy { @Override public void algorithmInterface() { // 具体的算法... } } public class ConcreteStrategyB implements Strategy { @Override public void algorithmInterface() { // 具体的算法... } } ``` ### 策略的创建 1. 因为策略模式会包含一组策略,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。我们可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。示例代码如下所示: ```java public class StrategyFactory { private static final Map strategies = new HashMap<>(); static { strategies.put("A", new ConcreteStrategyA()); strategies.put("B", new ConcreteStrategyB()); } public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } return strategies.get(type); } } ``` 2. 一般来讲,如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略对象是可以被共享使用的,不需要在每次调用 getStrategy() 的时候,都创建一个新的策略对象。针对这种情况,我们可以使用上面这种工厂类的实现方式,事先创建好每个策略对象,缓存到工厂类中,用的时候直接返回。 3. 相反,如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照如下方式来实现策略工厂类。 ```java public class StrategyFactory { public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } if (type.equals("A")) { return new ConcreteStrategyA(); } else if (type.equals("B")) { return new ConcreteStrategyB(); } return null; } } ``` ### 策略的使用 1. 刚刚讲了策略的定义和创建,现在,我们再来看一下,策略的使用。 2. 我们知道,策略模式包含一组可选策略,客户端代码一般如何确定使用哪个策略呢?最常见的是运行时动态确定使用哪种策略,这也是策略模式最典型的应用场景。 3. 这里的“运行时动态”指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。接下来,我们通过一个例子来解释一下。 ```java // 策略接口:EvictionStrategy // 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy... // 策略工厂:EvictionStrategyFactory public class UserCache { private Map cacheData = new HashMap<>(); private EvictionStrategy eviction; public UserCache(EvictionStrategy eviction) { this.eviction = eviction; } // ... } // 运行时动态确定,根据配置文件的配置决定使用哪种策略 public class Application { public static void main(String[] args) throws Exception { EvictionStrategy evictionStrategy = null; Properties props = new Properties(); props.load(new FileInputStream("./config.properties")); String type = props.getProperty("eviction_type"); evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type); UserCache userCache = new UserCache(evictionStrategy); // ... } } // 非运行时动态确定,在代码中指定使用哪种策略 public class Application { public static void main(String[] args) { // ... EvictionStrategy evictionStrategy = new LruEvictionStrategy(); UserCache userCache = new UserCache(evictionStrategy); // ... } } ``` 从上面的代码中,我们也可以看出,“非运行时动态确定”,也就是第二个 Application 中的使用方式,并不能发挥策略模式的优势。在这种应用场景下,策略模式实际上退化成了“面向对象的多态特性”或“基于接口而非实现编程原则”。 ## 如何利用策略模式避免分支判断? 1. 实际上,能够移除分支判断逻辑的模式不仅仅有策略模式,后面我们要讲的状态模式也可以。对于使用哪种模式,具体还要看应用场景来定。 策略模式适用于根据不同类型待动态,决定使用哪种策略这样一种应用场景。 2. 我们先通过一个例子来看下,if-else 或 switch-case 分支判断逻辑是如何产生的。具体的代码如下所示。在这个例子中,我们没有使用策略模式,而是将策略的定义、创建、使用直接耦合在一起。 ```java public class OrderService { public double discount(Order order) { double discount = 0.0; OrderType type = order.getType(); if (type.equals(OrderType.NORMAL)) { // 普通订单 // ...省略折扣计算算法代码 } else if (type.equals(OrderType.GROUPON)) { // 团购订单 // ...省略折扣计算算法代码 } else if (type.equals(OrderType.PROMOTION)) { // 促销订单 // ...省略折扣计算算法代码 } return discount; } } ``` 3. 如何来移除掉分支判断逻辑呢?那策略模式就派上用场了。我们使用策略模式对上面的代码重构,将不同类型订单的打折策略设计成策略类,并由工厂类来负责创建策略对象。具体的代码如下所示: ```java public interface DiscountStrategy { double calDiscount(Order order); } // 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码... // 策略的创建 public class DiscountStrategyFactory { private static final Map strategies = new HashMap<>(); static { strategies.put(OrderType.NORMAL, new NormalDiscountStrategy()); strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy()); strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy()); } public static DiscountStrategy getDiscountStrategy(OrderType type) { return strategies.get(type); } } // 策略的使用 public class OrderService { public double discount(Order order) { OrderType type = order.getType(); DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type); return discountStrategy.calDiscount(order); } } ``` 4. 重构之后的代码就没有了 if-else 分支判断语句了。实际上,这得益于策略工厂类。在工厂类中,我们用 Map 来缓存策略,根据 type 直接从 Map 中获取对应的策略,从而避免 if-else 分支判断逻辑。等后面讲到使用状态模式来避免分支判断逻辑的时候,你会发现,它们使用的是同样的套路。本质上都是借助“查表法”,根据 type 查表(代码中的 strategies 就是表)替代根据 type 分支判断。 5. 但是,如果业务场景需要每次都创建不同的策略对象,我们就要用另外一种工厂类的实现方式了。具体的代码如下所示: ```java public class DiscountStrategyFactory { public static DiscountStrategy getDiscountStrategy(OrderType type) { if (type == null) { throw new IllegalArgumentException("Type should not be null."); } if (type.equals(OrderType.NORMAL)) { return new NormalDiscountStrategy(); } else if (type.equals(OrderType.GROUPON)) { return new GrouponDiscountStrategy(); } else if (type.equals(OrderType.PROMOTION)) { return new PromotionDiscountStrategy(); } return null; } } ``` 这种实现方式相当于把原来的 if-else 分支逻辑,从 OrderService 类中转移到了工厂类中,实际上并没有真正将它移除。关于这个问题如何解决,我们在后面讲解。 ## 案例:文件排序 1. 上面,我们主要介绍了策略模式的原理和实现,以及如何利用策略模式来移除 if-else 或者 switch-case 分支判断逻辑。今天,我们结合“给文件排序”这样一个具体的例子,来详细讲一讲策略模式的设计意图和应用场景。 2. 除此之外,在今天的讲解中,我还会通过一步一步地分析、重构,给你展示一个设计模式是如何“创造”出来的。通过今天的学习,你会发现,**设计原则和思想其实比设计模式更加普适和重要,掌握了代码的设计原则和思想,我们甚至可以自己创造出来新的设计模式** ### 问题与解决思路 1. 假设有这样一个需求,希望写一个小程序,实现对一个文件进行排序的功能。文件中只包含整型数,并且,相邻的数字通过逗号来区隔。如果由你来编写这样一个小程序,你会如何来实现呢?你可以把它当作面试题,先自己思考一下,再来看我下面的讲解。 2. 你可能会说,这不是很简单嘛,只需要将文件中的内容读取出来,并且通过逗号分割成一个一个的数字,放到内存数组中,然后编写某种排序算法(比如快排),或者直接使用编程语言提供的排序函数,对数组进行排序,最后再将数组中的数据写入文件就可以了。 3. 但是,如果文件很大呢?比如有 10GB 大小,因为内存有限(比如只有 8GB 大小),我们没办法一次性加载文件中的所有数据到内存中,这个时候,我们就要利用外部排序算法 4. 如果文件更大,比如有 100GB 大小,我们为了利用 CPU 多核的优势,可以在外部排序的基础之上进行优化,加入多线程并发排序的功能,这就有点类似“单机版”的 MapReduce。 5. 如果文件非常大,比如有 1TB 大小,即便是单机多线程排序,这也算很慢了。这个时候,我们可以使用真正的 MapReduce 框架,利用多机的处理能力,提高排序的效率。 ### 代码实现与分析 1. 解决思路讲完了,不难理解。接下来,我们看一下,如何将解决思路翻译成代码实现。 2. 我先用最简单直接的方式实现将它实现出来。具体代码我贴在下面了,你可以先看一下。因为我们是在讲设计模式,不是讲算法,所以,在下面的代码实现中,我只给出了跟设计模式相关的骨架代码,并没有给出每种排序算法的具体代码实现。感兴趣的话,你可以自行实现一下。 ```java public 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多机排序 } } public class SortingTool { public static void main(String[] args) { Sorter sorter = new Sorter(); sorter.sortFile(args[0]); } } ``` 3. 在“编码规范”那一部分我们讲过,函数的行数不能过多,最好不要超过一屏的大小。所以,为了避免 sortFile() 函数过长,我们把每种排序算法从 sortFile() 函数中抽离出来,拆分成 4 个独立的排序函数。 4. 如果只是开发一个简单的工具,那上面的代码实现就足够了。毕竟,代码不多,后续修改、扩展的需求也不多,怎么写都不会导致代码不可维护。但是,如果我们是在开发一个大型项目,排序文件只是其中的一个功能模块,那我们就要在代码设计、代码质量上下点儿功夫了。只有每个小的功能模块都写好,整个项目的代码才能不差。 5. 在刚刚的代码中,我们并没有给出每种排序算法的代码实现。实际上,如果自己实现一下的话,你会发现,每种排序算法的实现逻辑都比较复杂,代码行数都比较多。所有排序算法的代码实现都堆在 Sorter 一个类中,这就会导致这个类的代码很多。而在“编码规范”那一部分中,我们也讲到,一个类的代码太多也会影响到可读性、可维护性。除此之外,所有的排序算法都设计成 Sorter 的私有函数,也会影响代码的可复用性。 ### 代码优化与重构 只要掌握了我们之前讲过的设计原则和思想,针对上面的问题,即便我们想不到该用什么设计模式来重构,也应该能知道该如何解决,那就是将 Sorter 类中的某些代码拆分出来,独立成职责更加单一的小类。实际上,拆分是应对类或者函数代码过多、应对代码复杂性的一个常用手段。按照这个解决思路,我们对代码进行重构。重构之后的代码如下所示: ```java 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) { // ... } } public class Sorter { 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); } } ``` 1. 经过拆分之后,每个类的代码都不会太多,每个类的逻辑都不会太复杂,代码的可读性、可维护性提高了。除此之外,我们将排序算法设计成独立的类,跟具体的业务逻辑(代码中的 if-else 那部分逻辑)解耦,也让排序算法能够复用。这一步实际上就是策略模式的第一步,也就是将策略的定义分离出来。 2. 实际上,上面的代码还可以继续优化。每种排序类都是无状态的,我们没必要在每次使用的时候,都重新创建一个新的对象。所以,我们可以使用工厂模式对对象的创建进行封装。按照这个思路,我们对代码进行重构。重构之后的代码如下所示: ```java public class SortAlgFactory { private static final Map 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 Sorter { 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”就是“表”。 ```java import java.io.File; import java.util.ArrayList; import java.util.List; public class Sorter { private static final long GB = 1000 * 1000 * 1000; private static final List 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; } } } ``` 1. 现在的代码实现就更加优美了。我们把可变的部分隔离到了策略工厂类和 Sorter 类中的静态代码段中。当要添加一个新的排序算法时,我们只需要修改策略工厂类和 Sort 类中的静态代码段,其他代码都不需要修改,这样就将代码改动最小化、集中化了。 2. 你可能会说,即便这样,当我们添加新的排序算法的时候,还是需要修改代码,并不完全符合开闭原则。有什么办法让我们完全满足开闭原则呢? 3. 对于 Java 语言来说,我们可以通过反射来避免对策略工厂类的修改。具体是这么做的:我们通过一个配置文件或者自定义的 annotation 来标注都有哪些策略类;策略工厂类读取配置文件或者搜索被 annotation 标注的策略类,然后通过反射了动态地加载这些策略类、创建策略对象;当我们新添加一个策略的时候,只需要将这个新添加的策略类添加到配置文件或者用 annotation 标注即可。还记得上面说的如何消除工厂类的if else吗?我们也可以用这种方法来解决。 4. 对于 Sorter 来说,我们可以通过同样的方法来避免修改。我们通过将文件大小区间和算法之间的对应关系放到配置文件中。当添加新的排序算法时,我们只需要改动配置文件即可,不需要改动代码。 # 职责链模式 1. 在前面,我们学习了模板模式、策略模式,今天,我们来学习职责链模式。这三种模式具有相同的作用:复用和扩展,在实际的项目开发中比较常用,特别是框架开发中,我们可以利用它们来提供框架的扩展点,能够让框架的使用者在不修改框架源码的情况下,基于扩展点定制化框架的功能。 2. 今天,我们主要讲解职责链模式的原理和实现。除此之外,我还会利用职责链模式,带你实现一个可以灵活扩展算法的敏感词过滤框架。下一节,我们会更加贴近实战,通过剖析 Servlet Filter、Spring Interceptor 来看,如何利用职责链模式实现框架中常用的过滤器、拦截器。 ## Demo案例-学校采购 > 需求 采购员采购教学器材 1. 如果金额小于等于 5000, 由教学主任审批 (0<=x<=5000) 2. 如果金额小于等于 10000, 由院长审批 (5000 传统方式解决方案 1. 传统方式是:接收到一个采购请求后,根据采购金额来调用对应的 Approver (审批人)完成审批。 2. 传统方式的问题分析 : 客户端这里会使用到 分支判断(比如 switch) 来对不同的采购请求处理, 这样就存在如下问题 (1) 如果各个级别的人员审批金额发生变化,在客户端的也需要变化 (2) 客户端必须明确的知道有多少个审批级别和访问 3. 这样对一个采购请求进行处理 和 Approver (审批人) 就存在强耦合关系,不利于代码的扩展和维护 > 职责链模式方案代码 ### Approver【抽象类】 ```java public abstract class Approver { Approver approver; // 下一个处理者 String name; // 名字 public Approver(String name) { // TODO Auto-generated constructor stub this.name = name; } // 下一个处理者 public void setApprover(Approver approver) { this.approver = approver; } // 处理审批请求的方法,得到一个请求, 处理是子类完成,因此该方法做成抽象 public abstract void processRequest(PurchaseRequest purchaseRequest); } ``` ### PurchaseRequest ```java // 请求类 public class PurchaseRequest { private int type = 0; // 请求类型 private float price = 0.0f; // 请求金额 private int id = 0; // 构造器 public PurchaseRequest(int type, float price, int id) { this.type = type; this.price = price; this.id = id; } public int getType() { return type; } public float getPrice() { return price; } public int getId() { return id; } } ``` ### DepartmentApprover ```java // 教学主任 public class DepartmentApprover extends Approver { public DepartmentApprover(String name) { // TODO Auto-generated constructor stub super(name); } @Override public void processRequest(PurchaseRequest purchaseRequest) { // TODO Auto-generated method stub if (purchaseRequest.getPrice() <= 5000) { System.out.println(" 请求编号 id= " + purchaseRequest.getId() + " 被 " + this.name + " 处理"); } else { approver.processRequest(purchaseRequest); } } } ``` ### CollegeApprover ```java // 院长 public class CollegeApprover extends Approver { public CollegeApprover(String name) { // TODO Auto-generated constructor stub super(name); } @Override public void processRequest(PurchaseRequest purchaseRequest) { // TODO Auto-generated method stub if (purchaseRequest.getPrice() > 5000 && purchaseRequest.getPrice() <= 10000) { System.out.println(" 请求编号 id= " + purchaseRequest.getId() + " 被 " + this.name + " 处理"); } else { approver.processRequest(purchaseRequest); } } } ``` ### ViceSchoolMasterApprover ```java // 副校长 public class ViceSchoolMasterApprover extends Approver { public ViceSchoolMasterApprover(String name) { // TODO Auto-generated constructor stub super(name); } @Override public void processRequest(PurchaseRequest purchaseRequest) { // TODO Auto-generated method stub if (purchaseRequest.getPrice() > 10000 && purchaseRequest.getPrice() <= 30000) { System.out.println(" 请求编号 id= " + purchaseRequest.getId() + " 被 " + this.name + " 处理"); } else { approver.processRequest(purchaseRequest); } } } ``` ### SchoolMasterApprover ```java // 校长 public class SchoolMasterApprover extends Approver { public SchoolMasterApprover(String name) { // TODO Auto-generated constructor stub super(name); } @Override public void processRequest(PurchaseRequest purchaseRequest) { // TODO Auto-generated method stub if (purchaseRequest.getPrice() > 30000) { System.out.println(" 请求编号 id= " + purchaseRequest.getId() + " 被 " + this.name + " 处理"); } else { approver.processRequest(purchaseRequest); } } } ``` ### Client ```java public class Client { public static void main(String[] args) { // TODO Auto-generated method stub // 创建一个请求 PurchaseRequest purchaseRequest = new PurchaseRequest(1, 4000, 1); // 创建相关的审批人 DepartmentApprover departmentApprover = new DepartmentApprover("张主任"); CollegeApprover collegeApprover = new CollegeApprover("李院长"); ViceSchoolMasterApprover viceSchoolMasterApprover = new ViceSchoolMasterApprover("王副校"); SchoolMasterApprover schoolMasterApprover = new SchoolMasterApprover("风校长"); // 需要将各个审批级别的下一个设置好 (处理人构成环形: ) departmentApprover.setApprover(collegeApprover); collegeApprover.setApprover(viceSchoolMasterApprover); viceSchoolMasterApprover.setApprover(schoolMasterApprover); schoolMasterApprover.setApprover(departmentApprover); departmentApprover.processRequest(purchaseRequest); viceSchoolMasterApprover.processRequest(purchaseRequest); } } ``` ## 职责链模式的原理和实现 1. 职责链模式的英文翻译是 Chain Of Responsibility Design Pattern。在 GoF 的《设计模式》中,它是这么定义的: > Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it. 2. 翻译成中文就是:将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。 3. 这么说比较抽象,我用更加容易理解的话来进一步解读一下。在职责链模式中,多个处理器(也就是刚刚定义中说的“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。 4. 关于职责链模式,我们先来看看它的代码实现。结合代码实现,你会更容易理解它的定义。职责链模式有多种实现方式,我们这里介绍两种比较常用的。 5. 第一种实现方式如下所示。其中,Handler 是所有处理器类的抽象父类,handle() 是抽象方法。每个具体的处理器类(HandlerA、HandlerB)的 handle() 函数的代码结构类似,如果它能处理该请求,就不继续往下传递;如果不能处理,则交由后面的处理器来处理(也就是调用 successor.handle())。HandlerChain 是处理器链,从数据结构的角度来看,它就是一个记录了链头、链尾的链表。其中,记录链尾是为了方便添加处理器。 ```java public abstract class Handler { protected Handler successor = null; public void setSuccessor(Handler successor) { this.successor = successor; } public abstract void handle(); } public class HandlerA extends Handler { @Override public boolean handle() { boolean handled = false; // ... if (!handled && successor != null) { successor.handle(); } } } public class HandlerB extends Handler { @Override public void handle() { boolean handled = false; // ... if (!handled && successor != null) { successor.handle(); } } } public class HandlerChain { private Handler head = null; private Handler tail = null; public void addHandler(Handler handler) { handler.setSuccessor(null); if (head == null) { head = handler; tail = handler; return; } tail.setSuccessor(handler); tail = handler; } public void handle() { if (head != null) { head.handle(); } } } // 使用举例 public class Application { public static void main(String[] args) { HandlerChain chain = new HandlerChain(); chain.addHandler(new HandlerA()); chain.addHandler(new HandlerB()); chain.handle(); } } ``` 6. 实际上,上面的代码实现不够优雅。处理器类的 handle() 函数,不仅包含自己的业务逻辑,还包含对下一个处理器的调用,也就是代码中的 successor.handle()。一个不熟悉这种代码结构的程序员,在添加新的处理器类的时候,很有可能忘记在 handle() 函数中调用 successor.handle(),这就会导致代码出现 bug。 7. 针对这个问题,我们对代码进行重构,利用模板模式,将调用 successor.handle() 的逻辑从具体的处理器类中剥离出来,放到抽象父类中。这样具体的处理器类只需要实现自己的业务逻辑就可以了。重构之后的代码如下所示: ```java public abstract class Handler { protected Handler successor = null; public void setSuccessor(Handler successor) { this.successor = successor; } public final void handle() { boolean handled = doHandle(); if (successor != null && !handled) { successor.handle(); } } protected abstract boolean doHandle(); } public class HandlerA extends Handler { @Override protected boolean doHandle() { boolean handled = false; // ... return handled; } } public class HandlerB extends Handler { @Override protected boolean doHandle() { boolean handled = false; // ... return handled; } } // HandlerChain和Application代码不变 ``` 我们再来看第二种实现方式,代码如下所示。这种实现方式更加简单。HandlerChain 类用数组而非链表来保存所有的处理器,并且需要在 HandlerChain 的 handle() 函数中,依次调用每个处理器的 handle() 函数。 ```java import java.util.ArrayList; import java.util.List; public interface IHandler { boolean handle(); } public class HandlerA implements IHandler { @Override public boolean handle() { boolean handled = false; // ... return handled; } } public class HandlerB implements IHandler { @Override public boolean handle() { boolean handled = false; // ... return handled; } } public class HandlerChain { private List handlers = new ArrayList<>(); public void addHandler(IHandler handler) { this.handlers.add(handler); } public void handle() { for (IHandler handler : handlers) { boolean handled = handler.handle(); if (handled) { break; } } } } // 使用举例 public class Application { public static void main(String[] args) { HandlerChain chain = new HandlerChain(); chain.addHandler(new HandlerA()); chain.addHandler(new HandlerB()); chain.handle(); } } ``` 在 GoF 给出的定义中,如果处理器链上的某个处理器能够处理这个请求,那就不会继续往下传递请求。实际上,职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况。这种变体也有两种实现方式:用链表存储处理器和用数组存储处理器,跟上面的两种实现方式类似,只需要稍微修改即可。我这里只给出其中一种实现方式,如下所示。另外一种实现方式你对照着上面的实现自行修改。我这里只给出其中一种实现方式,如下所示。另外一种实现方式你对照着上面的实现自行修改。 ```java public abstract class Handler { protected Handler successor = null; public void setSuccessor(Handler successor) { this.successor = successor; } public final void handle() { doHandle(); if (successor != null) { successor.handle(); } } protected abstract void doHandle(); } public class HandlerA extends Handler { @Override protected void doHandle() { // ... } } public class HandlerB extends Handler { @Override protected void doHandle() { // ... } } public class HandlerChain { private Handler head = null; private Handler tail = null; public void addHandler(Handler handler) { handler.setSuccessor(null); if (head == null) { head = handler; tail = handler; return; } tail.setSuccessor(handler); tail = handler; } public void handle() { if (head != null) { head.handle(); } } } // 使用举例 public class Application { public static void main(String[] args) { HandlerChain chain = new HandlerChain(); chain.addHandler(new HandlerA()); chain.addHandler(new HandlerB()); chain.handle(); } } ``` ## 职责链模式的应用场景举例 1. 职责链模式的原理和实现讲完了,我们再通过一个实际的例子,来学习一下职责链模式的应用场景。 2. 对于支持 UGC(User Generated Content,用户生成内容)的应用(比如论坛)来说,用户生成的内容(比如,在论坛中发表的帖子)可能会包含一些敏感词(比如涉黄、广告、反动等词汇)。针对这个应用场景,我们就可以利用职责链模式来过滤这些敏感词。 3. 对于包含敏感词的内容,我们有两种处理方式,一种是直接禁止发布,另一种是给敏感词打马赛克(比如,用 *** 替换敏感词)之后再发布。第一种处理方式符合 GoF 给出的职责链模式的定义,第二种处理方式是职责链模式的变体。 4. 我们这里只给出第一种实现方式的代码示例,如下所示,并且,我们只给出了代码实现的骨架,具体的敏感词过滤算法并没有给出。 ```java public interface SensitiveWordFilter { boolean doFilter(Content content); } public class SexyWordFilter implements SensitiveWordFilter { @Override public boolean doFilter(Content content) { boolean legal = true; // ... return legal; } } // PoliticalWordFilter、AdsWordFilter类代码结构与SexyWordFilter类似 public class SensitiveWordFilterChain { private List filters = new ArrayList<>(); public void addFilter(SensitiveWordFilter filter) { this.filters.add(filter); } // return true if content doesn't contain sensitive words. public boolean filter(Content content) { for (SensitiveWordFilter filter : filters) { if (!filter.doFilter(content)) { return false; } } return true; } } public class ApplicationDemo { public static void main(String[] args) { SensitiveWordFilterChain filterChain = new SensitiveWordFilterChain(); filterChain.addFilter(new AdsWordFilter()); filterChain.addFilter(new SexyWordFilter()); filterChain.addFilter(new PoliticalWordFilter()); boolean legal = filterChain.filter(new Content()); if (!legal) { // 不发表 } else { // 发表 } } } ``` 看了上面的实现,你可能会说,我像下面这样也可以实现敏感词过滤功能,而且代码更加简单,为什么非要使用职责链模式呢?这是不是过度设计呢? ```java public class SensitiveWordFilter { // return true if content doesn't contain sensitive words. public boolean filter(Content content) { if (!filterSexyWord(content)) { return false; } if (!filterAdsWord(content)) { return false; } if (!filterPoliticalWord(content)) { return false; } return true; } private boolean filterSexyWord(Content content) { // .... } private boolean filterAdsWord(Content content) { // ... } private boolean filterPoliticalWord(Content content) { // ... } } ``` 我们前面多次讲过,应用设计模式主要是为了应对代码的复杂性,让其满足开闭原则,提高代码的扩展性。这里应用职责链模式也不例外。实际上,我们在讲解策略模式的时候,也讲过类似的问题,比如,为什么要用策略模式?当时的给出的理由,与现在应用职责链模式的理由,几乎是一样的,你可以结合着当时的讲解一块来看下。 > 首先,我们来看,职责链模式如何应对代码的复杂性。 将大块代码逻辑拆分成函数,将大类拆分成小类,是应对代码复杂性的常用方法。应用职责链模式,我们把各个敏感词过滤函数继续拆分出来,设计成独立的类,进一步简化了 SensitiveWordFilter 类,让 SensitiveWordFilter 类的代码不会过多,过复杂。 > 其次,我们再来看,职责链模式如何让代码满足开闭原则,提高代码的扩展性。 1. 当我们要扩展新的过滤算法的时候,比如,我们还需要过滤特殊符号,按照非职责链模式的代码实现方式,我们需要修改 SensitiveWordFilter 的代码,违反开闭原则。不过,这样的修改还算比较集中,也是可以接受的。而职责链模式的实现方式更加优雅,只需要新添加一个 Filter 类,并且通过 addFilter() 函数将它添加到 FilterChain 中即可,其他代码完全不需要修改。 2. 不过,你可能会说,即便使用职责链模式来实现,当添加新的过滤算法的时候,还是要修改客户端代码(ApplicationDemo),这样做也没有完全符合开闭原则。 3. 实际上,细化一下的话,我们可以把上面的代码分成两类:框架代码和客户端代码。其中,ApplicationDemo 属于客户端代码,也就是使用框架的代码。除 ApplicationDemo 之外的代码属于敏感词过滤框架代码。 4. 假设敏感词过滤框架并不是我们开发维护的,而是我们引入的一个第三方框架,我们要扩展一个新的过滤算法,不可能直接去修改框架的源码。这个时候,利用职责链模式就能达到开篇所说的,在不修改框架源码的情况下,基于职责链模式提供的扩展点,来扩展新的功能。换句话说,我们在框架这个代码范围内实现了开闭原则。 5. 除此之外,利用职责链模式相对于不用职责链的实现方式,还有一个好处,那就是配置过滤算法更加灵活,可以只选择使用某几个过滤算法。 > 除此之外,我们还提到,职责链模式常用在框架的开发中,为框架提供扩展点,让框架的使用者在不修改框架源码的情况下,基于扩展点添加新的功能。实际上,更具体点来说,职责链模式最常用来开发框架的过滤器和拦截器。今天,我们就通过 Servlet Filter、Spring Interceptor 这两个 Java 开发中常用的组件,来具体讲讲它在框架开发中的应用。 ## Servlet Filter Servlet Filter 是 Java Servlet 规范中定义的组件,翻译成中文就是过滤器,它可以实现对 HTTP 请求的过滤功能,比如鉴权、限流、记录日志、验证参数等等。因为它是 Servlet 规范的一部分,所以,只要是支持 Servlet 的 Web 容器(比如,Tomcat、Jetty 等),都支持过滤器功能。为了帮助你理解,我画了一张示意图阐述它的工作原理,如下所示。 在实际项目中,我们该如何使用 Servlet Filter 呢?我写了一个简单的示例代码,如下所示。添加一个过滤器,我们只需要定义一个实现 javax.servlet.Filter 接口的过滤器类,并且将它配置在 web.xml 配置文件中。Web 容器启动的时候,会读取 web.xml 中的配置,创建过滤器对象。当有请求到来的时候,会先经过过滤器,然后才由 Servlet 来处理。 ```java public class LogFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // 在创建Filter时自动调用, // 其中filterConfig包含这个Filter的配置参数,比如name之类的(从配置文件中读取的) } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("拦截客户端发送来的请求."); chain.doFilter(request, response); System.out.println("拦截发送给客户端的响应."); } @Override public void destroy() { // 在销毁Filter时自动调用 } } ``` ```xml // 在web.xml配置文件中如下配置: logFilter com.xzg.cd.LogFilter logFilter /* ``` 1. 从刚刚的示例代码中,我们发现,添加过滤器非常方便,不需要修改任何代码,定义一个实现 javax.servlet.Filter 的类,再改改配置就搞定了,完全符合开闭原则。那 Servlet Filter 是如何做到如此好的扩展性的呢?我想你应该已经猜到了,它利用的就是职责链模式。现在,我们通过剖析它的源码,详细地看看它底层是如何实现的。 2. 在上一节中,我们讲到,职责链模式的实现包含处理器接口(IHandler)或抽象类(Handler),以及处理器链(HandlerChain)。对应到 Servlet Filter,javax.servlet.Filter 就是处理器接口,FilterChain 就是处理器链。接下来,我们重点来看 FilterChain 是如何实现的。 3. 不过,我们前面也讲过,Servlet 只是一个规范,并不包含具体的实现,所以,Servlet 中的 FilterChain 只是一个接口定义。具体的实现类由遵从 Servlet 规范的 Web 容器来提供,比如,ApplicationFilterChain 类就是 Tomcat 提供的 FilterChain 的实现类,源码如下所示。 4. 为了让代码更易读懂,我对代码进行了简化,只保留了跟设计思路相关的代码片段。完整的代码你可以自行去 Tomcat 中查看。 ```java public final class ApplicationFilterChain implements FilterChain { private int pos = 0; // 当前执行到了哪个filter private int n; // filter的个数 private ApplicationFilterConfig[] filters; private Servlet servlet; @Override public void doFilter(ServletRequest request, ServletResponse response) { if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; Filter filter = filterConfig.getFilter(); filter.doFilter(request, response, this); } else { // filter都处理完毕后,执行servlet servlet.service(request, response); } } public void addFilter(ApplicationFilterConfig filterConfig) { for (ApplicationFilterConfig filter : filters) if (filter == filterConfig) return; if (n == filters.length) { // 扩容 ApplicationFilterConfig[] newFilters = new ApplicationFilterConfig[n + INCREMENT]; System.arraycopy(filters, 0, newFilters, 0, n); filters = newFilters; } filters[n++] = filterConfig; } } ``` ApplicationFilterChain 中的 doFilter() 函数的代码实现比较有技巧,实际上是一个递归调用。你可以用每个 Filter(比如 LogFilter)的 doFilter() 的代码实现,直接替换 ApplicationFilterChain 的第 12 行代码,一眼就能看出是递归调用了。我替换了一下,如下所示。 ```java @Override public void doFilter(ServletRequest request, ServletResponse response) { if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; Filter filter = filterConfig.getFilter(); //filter.doFilter(request, response, this); //把filter.doFilter的代码实现展开替换到这里 System.out.println("拦截客户端发送来的请求."); chain.doFilter(request, response); // chain就是this System.out.println("拦截发送给客户端的响应.") } else { // filter都处理完毕后,执行servlet servlet.service(request, response); } } ``` 这样实现主要是为了在一个 doFilter() 方法中,支持双向拦截,既能拦截客户端发送来的请求,也能拦截发送给客户端的响应,你可以结合着 LogFilter 那个例子,以及对比待会要讲到的 Spring Interceptor,来自己理解一下。而我们上一节给出的两种实现方式,都没法做到在业务逻辑执行的前后,同时添加处理代码。 ## Spring Interceptor 1. 刚刚讲了 Servlet Filter,现在我们来讲一个功能上跟它非常类似的东西,Spring Interceptor,翻译成中文就是拦截器。尽管英文单词和中文翻译都不同,但这两者基本上可以看作一个概念,都用来实现对 HTTP 请求进行拦截处理。 2. 它们不同之处在于,Servlet Filter 是 Servlet 规范的一部分,实现依赖于 Web 容器。Spring Interceptor 是 Spring MVC 框架的一部分,由 Spring MVC 框架来提供实现。客户端发送的请求,会先经过 Servlet Filter,然后再经过 Spring Interceptor,最后到达具体的业务代码中。我画了一张图来阐述一个请求的处理流程,具体如下所示。 3. 在项目中,我们该如何使用 Spring Interceptor 呢?我写了一个简单的示例代码,如下所示。LogInterceptor 实现的功能跟刚才的 LogFilter 完全相同,只是实现方式上稍有区别。LogFilter 对请求和响应的拦截是在 doFilter() 一个函数中实现的,而 LogInterceptor 对请求的拦截在 preHandle() 中实现,对响应的拦截在 postHandle() 中实现。 ```java public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("拦截客户端发送来的请求."); return true; // 继续后续的处理 } @Override public void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("拦截发送给客户端的响应."); } @Override public void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("这里总是被执行."); } } ``` ```xml-dtd //在Spring MVC配置文件中配置interceptors ``` 4. 同样,我们还是来剖析一下,Spring Interceptor 底层是如何实现的。 5. 当然,它也是基于职责链模式实现的。其中,HandlerExecutionChain 类是职责链模式中的处理器链。它的实现相较于 Tomcat 中的 ApplicationFilterChain 来说,逻辑更加清晰,不需要使用递归来实现,主要是因为它将请求和响应的拦截工作,拆分到了两个函数中实现。HandlerExecutionChain 的源码如下所示,同样,我对代码也进行了一些简化,只保留了关键代码。 ```java public class HandlerExecutionChain { private final Object handler; private HandlerInterceptor[] interceptors; public void addInterceptor(HandlerInterceptor interceptor) { initInterceptorList().add(interceptor); } boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = 0; i < interceptors.length; i++) { HandlerInterceptor interceptor = interceptors[i]; if (!interceptor.preHandle(request, response, this.handler)) { triggerAfterCompletion(request, response, null); return false; } } } return true; } void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = interceptors.length - 1; i >= 0; i--) { HandlerInterceptor interceptor = interceptors[i]; interceptor.postHandle(request, response, this.handler, mv); } } } void triggerAfterCompletion( HttpServletRequest request, HttpServletResponse response, Exception ex) throws Exception { HandlerInterceptor[] interceptors = getInterceptors(); if (!ObjectUtils.isEmpty(interceptors)) { for (int i = this.interceptorIndex; i >= 0; i--) { HandlerInterceptor interceptor = interceptors[i]; try { interceptor.afterCompletion(request, response, this.handler, ex); } catch (Throwable ex2) { logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); } } } } } ``` 在 Spring 框架中,DispatcherServlet 的 doDispatch() 方法来分发请求,它在真正的业务逻辑执行前后,执行 HandlerExecutionChain 中的 applyPreHandle() 和 applyPostHandle() 函数,用来实现拦截的功能。具体的代码实现很简单,你自己应该能脑补出来,这里就不罗列了。感兴趣的话,你可以自行去查看。