From 0c16e8d9b1e690bdf218e8e05dd7999d9a2b4f9c Mon Sep 17 00:00:00 2001 From: youthlql <1826692270@qq.com> Date: Mon, 2 Aug 2021 15:40:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AE=BE=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F-=E8=A1=8C=E4=B8=BA=E5=9E=8B=20=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E4=B8=A4=E7=AF=87=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- .../设计模式-05.02-行为型-策略&职责链.md | 1442 +++++++++++++++++ .../设计模式-05.03-行为型-状态&迭代器.md | 1219 ++++++++++++++ 3 files changed, 2666 insertions(+), 1 deletion(-) create mode 100644 docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md create mode 100644 docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md diff --git a/README.md b/README.md index f1682cc..afce08b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ > 5、笔者会陆续更新,如果对你有所帮助,不妨[Github](https://github.com/youthlql/JavaYouth)点个**Star~**。你的**Star**是我创作的动力。 > > 6、所有更新日志,写作计划,公告等均在此发布 ==> [时间轴](https://imlql.cn/timeline/)。 +> +> 7、由于现在上班挺忙的,更新频率会下降。但是基本可以保证一个月一篇文章(我文章基本都是万字长文这种)。具体的看下[时间轴](https://imlql.cn/timeline/) @@ -147,7 +149,7 @@ AQS剩余部分,以及阻塞队列源码暂时先搁置一下。 -# 设计模式【更新中-7.14更新】 +# 设计模式【8.2更新基本完毕】 [1.设计模式-设计思想](docs/design_patterns/design_ideas/设计模式-01.设计思想.md) @@ -165,7 +167,9 @@ AQS剩余部分,以及阻塞队列源码暂时先搁置一下。 [5.设计模式-行为型-观察者&模板](docs/design_patterns/behavior_type/设计模式-05.01-行为型-观察者&模板.md) +[5.设计模式-行为型-策略&职责链](docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md) +[5.设计模式-行为型-状态&迭代器](docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md) # Netty diff --git a/docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md b/docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md new file mode 100644 index 0000000..f71a974 --- /dev/null +++ b/docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md @@ -0,0 +1,1442 @@ +--- +title: 设计模式-05.02-行为型-策略&职责链 +tags: + - 策略模式 + - 职责链模式 +categories: + - 设计模式 + - 05.行为型 +keywords: 策略模式,职责链模式 +description: 不多说,看文章 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.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() 函数,用来实现拦截的功能。具体的代码实现很简单,你自己应该能脑补出来,这里就不罗列了。感兴趣的话,你可以自行去查看。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md b/docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md new file mode 100644 index 0000000..8732396 --- /dev/null +++ b/docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md @@ -0,0 +1,1219 @@ +--- +title: 设计模式-05.03-行为型-状态&迭代器 +tags: + - 状态模式 + - 迭代器模式 +categories: + - 设计模式 + - 05.行为型 +keywords: 状态模式,迭代器模式 +description: 看文章 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +abbrlink: 877f4ef2 +date: 2021-08-02 15:51:58 +--- + + + +# 状态模式【重要】 + +1. 从今天起,我们开始学习状态模式。在实际的软件开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。从这一点上来看,它有点像我们之前讲到的组合模式。 +2. 状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。今天,我们就详细讲讲这几种实现方式,并且对比一下它们的优劣和应用场景。 + + + +## 什么是有限状态机? + +1. 有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。 +2. 对于刚刚给出的状态机的定义,我结合一个具体的例子,来进一步解释一下。 +3. “超级马里奥”游戏不知道你玩过没有?在游戏中,马里奥可以变身为多种形态,比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。 +4. 实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。 +5. 为了方便接下来的讲解,我对游戏背景做了简化,只保留了部分状态和事件。简化之后的状态转移如下图所示: + + + + + +6. 我们如何编程来实现上面的状态机呢?换句话说,如何将上面的状态转移图翻译成代码呢? +7. 我写了一个骨架代码,如下所示。其中,obtainMushRoom()、obtainCape()、obtainFireFlower()、meetMonster() 这几个函数,能够根据当前的状态和事件,更新状态和增减积分。不过,具体的代码实现我暂时并没有给出。你可以把它当做面试题,试着补全一下,然后再来看讲解,这样你的收获会更大。 + +```java +public enum State { + SMALL(0), + SUPER(1), + FIRE(2), + CAPE(3); + + private int value; + + private State(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } +} + +public class MarioStateMachine { + + private int score; + private State currentState; + + public MarioStateMachine() { + this.score = 0; + this.currentState = State.SMALL; + } + + public void obtainMushRoom() { + // TODO + } + + public void obtainCape() { + // TODO + } + + public void obtainFireFlower() { + // TODO + } + + public void meetMonster() { + // TODO + } + + public int getScore() { + return this.score; + } + + public State getCurrentState() { + return this.currentState; + } +} + +public class ApplicationDemo { + + public static void main(String[] args) { + MarioStateMachine mario = new MarioStateMachine(); + mario.obtainMushRoom(); + int score = mario.getScore(); + State state = mario.getCurrentState(); + System.out.println("mario score: " + score + "; state: " + state); + } +} +``` + + + +## 状态机实现方式一:分支逻辑法 + +1. 对于如何实现状态机,我总结了三种方式。其中,最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以,我把这种方法暂且命名为分支逻辑法。 +2. 按照这个实现思路,我将上面的骨架代码补全一下。补全之后的代码如下所示: + +```java +public class MarioStateMachine { + + private int score; + private State currentState; + + public MarioStateMachine() { + this.score = 0; + this.currentState = State.SMALL; + } + + public void obtainMushRoom() { + if (currentState.equals(State.SMALL)) { + this.currentState = State.SUPER; + this.score += 100; + } + } + + public void obtainCape() { + if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER)) { + this.currentState = State.CAPE; + this.score += 200; + } + } + + public void obtainFireFlower() { + if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER)) { + this.currentState = State.FIRE; + this.score += 300; + } + } + + public void meetMonster() { + if (currentState.equals(State.SUPER)) { + this.currentState = State.SMALL; + this.score -= 100; + return; + } + + if (currentState.equals(State.CAPE)) { + this.currentState = State.SMALL; + this.score -= 200; + return; + } + + if (currentState.equals(State.FIRE)) { + this.currentState = State.SMALL; + this.score -= 300; + return; + } + } + + public int getScore() { + return this.score; + } + + public State getCurrentState() { + return this.currentState; + } +} +``` + +对于简单的状态机来说,分支逻辑这种实现方式是可以接受的。但是,对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,我们要在冗长的分支逻辑中找到对应的代码进行修改,很容易改错,引入 bug。 + + + +## 状态机实现方式二:查表法 + +1. 实际上,上面这种实现方法有点类似 hard code,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。接下来,我们就一块儿来看下,如何利用查表法来补全骨架代码。 +2. 实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。 + + + +3. 相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transitionTable 和 actionTable 两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。具体的代码如下所示: + +```java +public enum Event { + GOT_MUSHROOM(0), + GOT_CAPE(1), + GOT_FIRE(2), + MET_MONSTER(3); + + private int value; + + private Event(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } +} + +public class MarioStateMachine { + private int score; + private State currentState; + + private static final State[][] transitionTable = { + {SUPER, CAPE, FIRE, SMALL}, + {SUPER, CAPE, FIRE, SMALL}, + {CAPE, CAPE, CAPE, SMALL}, + {FIRE, FIRE, FIRE, SMALL} + }; + + private static final int[][] actionTable = { + {+100, +200, +300, +0}, + {+0, +200, +300, -100}, + {+0, +0, +0, -200}, + {+0, +0, +0, -300} + }; + + public MarioStateMachine() { + this.score = 0; + this.currentState = State.SMALL; + } + + public void obtainMushRoom() { + executeEvent(Event.GOT_MUSHROOM); + } + + public void obtainCape() { + executeEvent(Event.GOT_CAPE); + } + + public void obtainFireFlower() { + executeEvent(Event.GOT_FIRE); + } + + public void meetMonster() { + executeEvent(Event.MET_MONSTER); + } + + private void executeEvent(Event event) { + int stateValue = currentState.getValue(); + int eventValue = event.getValue(); + this.currentState = transitionTable[stateValue][eventValue]; + this.score = actionTable[stateValue][eventValue]; + } + + public int getScore() { + return this.score; + } + + public State getCurrentState() { + return this.currentState; + } +} +``` + + + +## 状态机实现方式三:状态模式 + +1. 在查表法的代码实现中,事件触发的动作只是简单的积分加减,所以,我们用一个 int 类型的二维数组 actionTable 就能表示,二维数组中的值表示积分的加减值。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),我们就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性。 +2. 虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等。实际上,针对分支逻辑法存在的问题,我们可以使用状态模式来解决。 +3. 状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。我们还是结合代码来理解这句话。 +4. 利用状态模式,我们来补全 MarioStateMachine 类,补全后的代码如下所示。 +5. 其中,IMario 是状态的接口,定义了所有的事件。SmallMario、SuperMario、CapeMario、FireMario 是 IMario 接口的实现类,分别对应状态机中的 4 个状态。原来所有的状态转移和动作执行的代码逻辑,都集中在 MarioStateMachine 类中,现在,这些代码逻辑被分散到了这 4 个状态类中。 + + + +```java +public interface IMario { // 所有状态类的接口 + State getName(); + // 以下是定义的事件 + void obtainMushRoom(); + void obtainCape(); + void obtainFireFlower(); + void meetMonster(); +} + +public class SmallMario implements IMario { + private MarioStateMachine stateMachine; + + public SmallMario(MarioStateMachine stateMachine) { + this.stateMachine = stateMachine; + } + + @Override + public State getName() { + return State.SMALL; + } + + @Override + public void obtainMushRoom() { + stateMachine.setCurrentState(new SuperMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 100); + } + + @Override + public void obtainCape() { + stateMachine.setCurrentState(new CapeMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 200); + } + + @Override + public void obtainFireFlower() { + stateMachine.setCurrentState(new FireMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 300); + } + + @Override + public void meetMonster() { + // do nothing... + } +} + +public class SuperMario implements IMario { + private MarioStateMachine stateMachine; + + public SuperMario(MarioStateMachine stateMachine) { + this.stateMachine = stateMachine; + } + + @Override + public State getName() { + return State.SUPER; + } + + @Override + public void obtainMushRoom() { + // do nothing... + } + + @Override + public void obtainCape() { + stateMachine.setCurrentState(new CapeMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 200); + } + + @Override + public void obtainFireFlower() { + stateMachine.setCurrentState(new FireMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 300); + } + + @Override + public void meetMonster() { + stateMachine.setCurrentState(new SmallMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() - 100); + } +} + +// 省略CapeMario、FireMario类... + +public class MarioStateMachine { + private int score; + private IMario currentState; // 不再使用枚举来表示状态 + + public MarioStateMachine() { + this.score = 0; + this.currentState = new SmallMario(this); + } + + public void obtainMushRoom() { + this.currentState.obtainMushRoom(); + } + + public void obtainCape() { + this.currentState.obtainCape(); + } + + public void obtainFireFlower() { + this.currentState.obtainFireFlower(); + } + + public void meetMonster() { + this.currentState.meetMonster(); + } + + public int getScore() { + return this.score; + } + + public State getCurrentState() { + return this.currentState.getName(); + } + + public void setScore(int score) { + this.score = score; + } + + public void setCurrentState(IMario currentState) { + this.currentState = currentState; + } +} +``` + +1. 上面的代码实现不难看懂,我只强调其中的一点,即 MarioStateMachine 和各个状态类之间是双向依赖关系。MarioStateMachine 依赖各个状态类是理所当然的,但是,反过来,各个状态类为什么要依赖 MarioStateMachine 呢?这是因为,各个状态类需要更新 MarioStateMachine 中的两个变量,score 和 currentState。 +2. 实际上,上面的代码还可以继续优化,我们可以将状态类设计成单例,毕竟状态类中不包含任何成员变量。但是,当将状态类设计成单例之后,我们就无法通过构造函数来传递 MarioStateMachine 了,而状态类又要依赖 MarioStateMachine,那该如何解决这个问题呢? +3. 单例模式的讲解中,我们提到过几种解决方法,你可以回过头去再查看一下。在这里,我们可以通过函数参数将 MarioStateMachine 传递进状态类。根据这个设计思路,我们对上面的代码进行重构。重构之后的代码如下所示: + +```java +public interface IMario { + State getName(); + void obtainMushRoom(MarioStateMachine stateMachine); + void obtainCape(MarioStateMachine stateMachine); + void obtainFireFlower(MarioStateMachine stateMachine); + void meetMonster(MarioStateMachine stateMachine); +} + +public class SmallMario implements IMario { + private static final SmallMario instance = new SmallMario(); + private SmallMario() {} + + public static SmallMario getInstance() { + return instance; + } + + @Override + public State getName() { + return State.SMALL; + } + + @Override + public void obtainMushRoom(MarioStateMachine stateMachine) { + stateMachine.setCurrentState(SuperMario.getInstance()); + stateMachine.setScore(stateMachine.getScore() + 100); + } + + @Override + public void obtainCape(MarioStateMachine stateMachine) { + stateMachine.setCurrentState(CapeMario.getInstance()); + stateMachine.setScore(stateMachine.getScore() + 200); + } + + @Override + public void obtainFireFlower(MarioStateMachine stateMachine) { + stateMachine.setCurrentState(FireMario.getInstance()); + stateMachine.setScore(stateMachine.getScore() + 300); + } + + @Override + public void meetMonster(MarioStateMachine stateMachine) { + // do nothing... + } +} + +// 省略SuperMario、CapeMario、FireMario类... + +public class MarioStateMachine { + private int score; + private IMario currentState; + + public MarioStateMachine() { + this.score = 0; + this.currentState = SmallMario.getInstance(); + } + + public void obtainMushRoom() { + this.currentState.obtainMushRoom(this); + } + + public void obtainCape() { + this.currentState.obtainCape(this); + } + + public void obtainFireFlower() { + this.currentState.obtainFireFlower(this); + } + + public void meetMonster() { + this.currentState.meetMonster(this); + } + + public int getScore() { + return this.score; + } + + public State getCurrentState() { + return this.currentState.getName(); + } + + public void setScore(int score) { + this.score = score; + } + + public void setCurrentState(IMario currentState) { + this.currentState = currentState; + } +} +``` + +实际上,像游戏这种比较复杂的状态机,包含的状态比较多,我优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现。 + + + + + +# 迭代器模式【重要】 + +1. 今天,我们学习另外一种行为型设计模式,迭代器模式。它用来遍历集合对象。不过,很多编程语言都将迭代器作为一个基础的类库,直接提供出来了。在平时开发中,特别是业务开发,我们直接使用即可,很少会自己去实现一个迭代器。不过,知其然知其所以然,弄懂原理能帮助我们更好的使用这些工具类,所以,我觉得还是有必要学习一下这个模式。 +2. 我们知道,大部分编程语言都提供了多种遍历集合的方式,比如 for 循环、foreach 循环、迭代器等。所以,今天我们除了讲解迭代器的原理和实现之外,还会重点讲一下,相对于其他遍历方式,利用迭代器来遍历集合的优势。 + + + +## 迭代器模式的原理和实现 + +1. 迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern)。 +2. 在开篇中我们讲到,它用来遍历集合对象。这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。 +3. 迭代器是用来遍历容器的,所以,一个完整的迭代器模式一般会涉及**容器和容器迭代器**部分两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。对于迭代器模式,我画了一张简单的类图,你可以看一看,先有个大致的印象。 + + + + + +4. 接下来,我们通过一个例子来具体讲,如何实现一个迭代器。 +5. 开篇中我们有提到,大部分编程语言都提供了遍历容器的迭代器类,我们在平时开发中,直接拿来用即可,几乎不大可能从零编写一个迭代器。不过,这里为了讲解迭代器的实现原理,我们假设某个新的编程语言的基础类库中,还没有提供线性容器对应的迭代器,需要我们从零开始开发。现在,我们一块来看具体该如何去做。 +6. 我们知道,线性数据结构包括数组和链表,在大部分编程语言中都有对应的类来封装这两种数据结构,在开发中直接拿来用就可以了。假设在这种新的编程语言中,这两个数据结构分别对应 ArrayList 和 LinkedList 两个类。除此之外,我们从两个类中抽象出公共的接口,定义为 List 接口,以方便开发者基于接口而非实现编程,编写的代码能在两种数据存储结构之间灵活切换。 +7. 现在,我们针对 ArrayList 和 LinkedList 两个线性容器,设计实现对应的迭代器。按照之前给出的迭代器模式的类图,我们定义一个迭代器接口 Iterator,以及针对两种容器的具体的迭代器实现类 ArrayIterator 和 ListIterator。 +8. 我们先来看下 Iterator 接口的定义。具体的代码如下所示: + +```java +// 接口定义方式一 +public interface Iterator { + boolean hasNext(); + void next(); + E currentItem(); +} + +// 接口定义方式二 +public interface Iterator { + boolean hasNext(); + E next(); +} +``` + +9. Iterator 接口有两种定义方式。 +10. 在第一种定义中,next() 函数用来将游标后移一位元素,currentItem() 函数用来返回当前游标指向的元素。在第二种定义中,返回当前元素与后移一位这两个操作,要放到同一个函数 next() 中完成。 +11. 第一种定义方式更加灵活一些,比如我们可以多次调用 currentItem() 查询当前元素,而不移动游标。所以,在接下来的实现中,我们选择第一种接口定义方式。 +12. 现在,我们再来看下 ArrayIterator 的代码实现,具体如下所示。代码实现非常简单,不需要太多解释。你可以结合着我给出的 demo,自己理解一下。 + + + +```java +public class ArrayIterator implements Iterator { + private int cursor; + private ArrayList arrayList; + + public ArrayIterator(ArrayList arrayList) { + this.cursor = 0; + this.arrayList = arrayList; + } + + @Override + public boolean hasNext() { + return cursor != arrayList.size(); // 注意这里,cursor在指向最后一个元素的时候,hasNext()仍旧返回true。 + } + + @Override + public void next() { + cursor++; + } + + @Override + public E currentItem() { + if (cursor >= arrayList.size()) { + throw new NoSuchElementException(); + } + + return arrayList.get(cursor); + } +} + +public class Demo { + + public static void main(String[] args) { + ArrayList names = new ArrayList<>(); + names.add("lql"); + names.add("dl"); + names.add("tql"); + + Iterator iterator = new ArrayIterator(names); + while (iterator.hasNext()) { + System.out.println(iterator.currentItem()); + iterator.next(); + } + } +} +``` + +13. 在上面的代码实现中,我们需要将待遍历的容器对象,通过构造函数传递给迭代器类。实际上,为了封装迭代器的创建细节,我们可以在容器中定义一个 iterator() 方法,来创建对应的迭代器。为了能实现基于接口而非实现编程,我们还需要将这个方法定义在 List 接口中。具体的代码实现和使用示例如下所示: + +```java +import java.util.Iterator; + +public interface List { + Iterator iterator(); + // ...省略其他接口函数... +} + +public class ArrayList implements List { + // ... + public Iterator iterator() { + return new ArrayIterator(this); + } + // ...省略其他代码 +} + +public class Demo { + + public static void main(String[] args) { + List names = new ArrayList<>(); + names.add("lql"); + names.add("dl"); + names.add("tql"); + + Iterator iterator = names.iterator(); + while (iterator.hasNext()) { + System.out.println(iterator.currentItem()); + iterator.next(); + } + } +} +``` + +1. 对于 LinkedIterator,它的代码结构跟 ArrayIterator 完全相同,我这里就不给出具体的代码实现了,你可以参照 ArrayIterator 自己去写一下。 +2. 结合刚刚的例子,我们来总结一下迭代器的设计思路。总结下来就三句话:迭代器中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器。 +3. 这里我画了一张类图,如下所示。实际上就是对上面那张类图的细化,你可以结合着一块看。 + + + + + +## 迭代器模式的优势 + +1. 迭代器的原理和代码实现讲完了。接下来,我们来一块看一下,使用迭代器遍历集合的优势。 +2. 一般来讲,遍历集合数据有三种方法:for 循环、foreach 循环、iterator 迭代器。对于这三种方式,我拿 Java 语言来举例说明一下。具体的代码如下所示: + +```java +public static void main(String[] args) { + List names = new ArrayList<>(); + + names.add("lql"); + names.add("dl"); + names.add("tql"); + + // 第一种遍历方式:for循环 + for (int i = 0; i < names.size(); i++) { + System.out.print(names.get(i) + ","); + } + + // 第二种遍历方式:foreach循环 + for (String name : names) { + System.out.print(name + ","); + } + + // 第三种遍历方式:迭代器遍历 + Iterator iterator = names.iterator(); + while (iterator.hasNext()) { + System.out.print(iterator.next() + ","); // Java中的迭代器接口是第二种定义方式,next()既移动游标又返回数据 + } + } +``` + +3. 实际上,foreach 循环只是一个语法糖而已,底层是基于迭代器来实现的。也就是说,上面代码中的第二种遍历方式(foreach 循环代码)的底层实现,就是第三种遍历方式(迭代器遍历代码)。这两种遍历方式可以看作同一种遍历方式,也就是迭代器遍历方式。 +4. 从上面的代码来看,for 循环遍历方式比起迭代器遍历方式,代码看起来更加简洁。那我们为什么还要用迭代器来遍历容器呢?为什么还要给容器设计对应的迭代器呢?原因有以下三个。 +5. 首先,对于类似数组和链表这样的数据结构,遍历方式比较简单,直接使用 for 循环来遍历就足够了。但是,对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性。 +6. 前面也多次提到,应对复杂性的方法就是拆分。我们可以将遍历操作拆分到迭代器类中。比如,针对图的遍历,我们就可以定义 DFSIterator、BFSIterator 两个迭代器类,让它们分别来实现深度优先遍历和广度优先遍历。 +7. 其次,将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。这样,我们就可以创建多个不同的迭代器,同时对同一个容器进行遍历而互不影响。 +8. 最后,容器和迭代器都提供了抽象的接口,方便我们在开发的时候,基于接口而非具体的实现编程。当需要切换新的遍历算法的时候,比如,从前往后遍历链表切换成从后往前遍历链表,客户端代码只需要将迭代器类从 LinkedIterator 切换为 ReversedLinkedIterator 即可,其他代码都不需要修改。除此之外,添加新的遍历算法,我们只需要扩展新的迭代器类,也更符合开闭原则。 + + + +1. 我们通过给 ArrayList、LinkedList 容器实现迭代器,学习了迭代器模式的原理、实现和设计意图。迭代器模式主要作用是解耦容器代码和遍历代码,这也印证了我们前面多次讲过的应用设计模式的主要目的是解耦。 +2. 上面讲解的内容都比较基础,现在,我们来深挖一下,如果在使用迭代器遍历集合的同时增加、删除集合中的元素,会发生什么情况?应该如何应对?如何在遍历的同时安全地删除集合元素? + + + +## 在遍历的同时增删集合元素会发生什么? + +1. 在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有情况下都会遍历出错,有的时候也可以正常遍历,所以,这种行为称为**结果不可预期行为**或者**未决行为**,也就是说,运行结果到底是对还是错,要视情况而定。 +2. 怎么理解呢?我们通过一个例子来解释一下。我们还是延续上一节实现的 ArrayList 迭代器的例子。为了方便你查看,我把相关的代码都重新拷贝到这里了。 + +```java + +public interface Iterator { + boolean hasNext(); + void next(); + E currentItem(); +} + +public class ArrayIterator implements Iterator { + private int cursor; + private ArrayList arrayList; + + public ArrayIterator(ArrayList arrayList) { + this.cursor = 0; + this.arrayList = arrayList; + } + + @Override + public boolean hasNext() { + return cursor < arrayList.size(); + } + + @Override + public void next() { + cursor++; + } + + @Override + public E currentItem() { + if (cursor >= arrayList.size()) { + throw new NoSuchElementException(); + } + + return arrayList.get(cursor); + } +} + +public interface List { + Iterator iterator(); +} + +public class ArrayList implements List { + // ... + public Iterator iterator() { + return new ArrayIterator(this); + } + // ... +} + +public class Demo { + + public static void main(String[] args) { + List names = new ArrayList<>(); + names.add("a"); + names.add("b"); + names.add("c"); + names.add("d"); + + Iterator iterator = names.iterator(); + iterator.next(); + names.remove("a"); + } +} +``` + +3. 我们知道,ArrayList 底层对应的是数组这种数据结构,在执行完第 55 行代码的时候,数组中存储的是 a、b、c、d 四个元素,迭代器的游标 cursor 指向元素 a。当执行完第 56 行代码的时候,游标指向元素 b,到这里都没有问题。 +4. 为了保持数组存储数据的连续性,数组的删除操作会涉及元素的搬移。当执行到第 57 行代码的时候,我们从数组中将元素 a 删除掉,b、c、d 三个元素会依次往前搬移一位,这就会导致游标本来指向元素 b,现在变成了指向元素 c。原本在执行完第 56 行代码之后,我们还可以遍历到 b、c、d 三个元素,但在执行完第 57 行代码之后,我们只能遍历到 c、d 两个元素,b 遍历不到了。 +5. 对于上面的描述,我画了一张图,你可以对照着理解。 + + + +6. 不过,如果第 57 行代码删除的不是游标前面的元素(元素 a)以及游标所在位置的元素(元素 b),而是游标后面的元素(元素 c 和 d),这样就不会存在任何问题了,不会存在某个元素遍历不到的情况了。 +7. 所以,我们前面说,在遍历的过程中删除集合元素,结果是不可预期的,有时候没问题(删除元素 c 或 d),有时候就有问题(删除元素 a 或 b),这个要视情况而定(到底删除的是哪个位置的元素),就是这个意思。 +8. 在遍历的过程中删除集合元素,有可能会导致某个元素遍历不到,那在遍历的过程中添加集合元素,会发生什么情况呢?还是结合刚刚那个例子来讲解,我们将上面的代码稍微改造一下,把删除元素改为添加元素。具体的代码如下所示: + +```java +public class Demo { + public static void main(String[] args) { + List names = new ArrayList<>(); + names.add("a"); + names.add("b"); + names.add("c"); + names.add("d"); + + Iterator iterator = names.iterator(); + iterator.next(); + names.add(0, "x"); + } +} +``` + +9. 在执行完第 10 行代码之后,数组中包含 a、b、c、d 四个元素,游标指向 b 这个元素,已经跳过了元素 a。在执行完第 11 行代码之后,我们将 x 插入到下标为 0 的位置,a、b、c、d 四个元素依次往后移动一位。这个时候,游标又重新指向了元素 a。元素 a 被游标重复指向两次,也就是说,元素 a 存在被重复遍历的情况。 +10. 跟删除情况类似,如果我们在游标的后面添加元素,就不会存在任何问题。所以,在遍历的同时添加集合元素也是一种不可预期行为。 +11. 同样,对于上面的添加元素的情况,我们也画了一张图,如下所示,你可以对照着理解。 + + + + + +## 如何应对遍历时改变集合导致的未决行为? + +1. 当通过迭代器来遍历集合的时候,增加、删除集合元素会导致不可预期的遍历结果。实际上,“不可预期”比直接出错更加可怕,有的时候运行正确,有的时候运行错误,一些隐藏很深、很难 debug 的 bug 就是这么产生的。那我们如何才能避免出现这种不可预期的运行结果呢? +2. 有两种比较干脆利索的解决方案:一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错。 +3. 实际上,第一种解决方案比较难实现,我们要确定遍历开始和结束的时间点。遍历开始的时间节点我们很容易获得。我们可以把创建迭代器的时间点作为遍历开始的时间点。但是,遍历结束的时间点该如何来确定呢? +4. 你可能会说,遍历到最后一个元素的时候就算结束呗。但是,在实际的软件开发中,每次使用迭代器来遍历元素,并不一定非要把所有元素都遍历一遍。如下所示,我们找到一个值为 b 的元素就提前结束了遍历。 + +```java +public class Demo { + + public static void main(String[] args) { + List names = new ArrayList<>(); + names.add("a"); + names.add("b"); + names.add("c"); + names.add("d"); + + Iterator iterator = names.iterator(); + while (iterator.hasNext()) { + String name = iterator.currentItem(); + if (name.equals("b")) { + break; + } + } + } +} +``` + +5. 你可能还会说,那我们可以在迭代器类中定义一个新的接口 finishIteration(),主动告知容器迭代器使用完了,你可以增删元素了,示例代码如下所示。但是,这就要求程序员在使用完迭代器之后要主动调用这个函数,也增加了开发成本,还很容易漏掉。 + +```java +public class Demo { + + public static void main(String[] args) { + List names = new ArrayList<>(); + names.add("a"); + names.add("b"); + names.add("c"); + names.add("d"); + + Iterator iterator = names.iterator(); + while (iterator.hasNext()) { + String name = iterator.currentItem(); + if (name.equals("b")) { + iterator.finishIteration(); // 主动告知容器这个迭代器用完了 + break; + } + } + } +} +``` + +6. 实际上,第二种解决方法更加合理。Java 语言就是采用的这种解决方案,增删元素之后,让遍历报错。接下来,我们具体来看一下如何实现。 +7. 怎么确定在遍历时候,集合有没有增删元素呢?我们在 ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1。当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 值传递给迭代器的 expectedModCount 成员变量,之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,我们都会检查集合上的 modCount 是否等于 expectedModCount,也就是看,在创建完迭代器之后,modCount 是否改变过。 +8. 如果两个值不相同,那就说明集合存储的元素已经改变了,要么增加了元素,要么删除了元素,之前创建的迭代器已经不能正确运行了,再继续使用就会产生不可预期的结果,所以我们选择 fail-fast 解决方式,抛出运行时异常,结束掉程序,让程序员尽快修复这个因为不正确使用迭代器而产生的 bug。 +9. 上面的描述翻译成代码就是下面这样子。你可以结合着代码一起理解我刚才的讲解。 + +```java +public class ArrayIterator implements Iterator { + private int cursor; + private ArrayList arrayList; + private int expectedModCount; + + public ArrayIterator(ArrayList arrayList) { + this.cursor = 0; + this.arrayList = arrayList; + this.expectedModCount = arrayList.modCount; + } + + @Override + public boolean hasNext() { + checkForComodification(); + return cursor < arrayList.size(); + } + + @Override + public void next() { + checkForComodification(); + cursor++; + } + + @Override + public Object currentItem() { + checkForComodification(); + return arrayList.get(cursor); + } + + private void checkForComodification() { + if (arrayList.modCount != expectedModCount) throw new ConcurrentModificationException(); + } +} + +// 代码示例 +public class Demo { + + public static void main(String[] args) { + List names = new ArrayList<>(); + names.add("a"); + names.add("b"); + names.add("c"); + names.add("d"); + + Iterator iterator = names.iterator(); + iterator.next(); + names.remove("a"); + iterator.next(); // 抛出ConcurrentModificationException异常 + } +} +``` + + + +## 如何在遍历的同时安全地删除集合元素? + +1. 像 Java 语言,迭代器类中除了前面提到的几个最基本的方法之外,还定义了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素。不过,需要说明的是,它并没有提供添加元素的方法。毕竟迭代器的主要作用是遍历,添加元素放到迭代器里本身就不合适。 +2. 我个人觉得,Java 迭代器中提供的 remove() 方法还是比较鸡肋的,作用有限。它只能删除游标指向的前一个元素,而且一个 next() 函数之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错。我还是通过一个例子来解释一下。 + +```java +public class Demo { + public static void main(String[] args) { + List names = new ArrayList<>(); + names.add("a"); + names.add("b"); + names.add("c"); + names.add("d"); + + Iterator iterator = names.iterator(); + iterator.next(); + iterator.remove(); + iterator.remove(); // 报错,抛出IllegalStateException异常 + } +} +``` + +3. 现在,我们一块来看下,为什么通过迭代器就能安全的删除集合中的元素呢?源码之下无秘密。我们来看下 remove() 函数是如何实现的,代码如下所示。稍微提醒一下,在 Java 实现中,迭代器类是容器类的内部类,并且 next() 函数不仅将游标后移一位,还会返回当前的元素。 + +```java +public class ArrayList { + transient Object[] elementData; + private int size; + + public Iterator iterator() { + return new Itr(); + } + + private class Itr implements Iterator { + int cursor; // index of next element to return + int lastRet = -1; // index of last element returned; -1 if no such + int expectedModCount = modCount; + + Itr() {} + + public boolean hasNext() { + return cursor != size; + } + + @SuppressWarnings("unchecked") + public E next() { + checkForComodification(); + int i = cursor; + if (i >= size) throw new NoSuchElementException(); + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) throw new ConcurrentModificationException(); + cursor = i + 1; + + return (E) elementData[lastRet = i]; + } + + public void remove() { + if (lastRet < 0) throw new IllegalStateException(); + checkForComodification(); + + try { + ArrayList.this.remove(lastRet); + cursor = lastRet; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } + } + } +} +``` + +在上面的代码实现中,迭代器类新增了一个 lastRet 成员变量,用来记录游标指向的前一个元素。通过迭代器去删除这个元素的时候,我们可以更新迭代器中的游标和 lastRet 值,来保证不会因为删除元素而导致某个元素遍历不到。如果通过容器来删除元素,并且希望更新迭代器中的游标值来保证遍历不出错,我们就要维护这个容器都创建了哪些迭代器,每个迭代器是否还在使用等信息,代码实现就变得比较复杂了。 + + + +## 如何设计实现一个支持“快照”功能的iterator? + +今天,我们再来看这样一个问题:如何实现一个支持“快照”功能的迭代器?这个问题算是对上一节内容的延伸思考,为的是帮你加深对迭代器模式的理解,也是对你分析、解决问题的一种锻炼。你可以把它当作一个面试题或者练习题,在讲解之前,先试一试自己能否顺利回答上来。 + + + +### 问题描述 + +1. 我们先来介绍一下问题的背景:如何实现一个支持“快照”功能的迭代器模式? +2. 理解这个问题最关键的是理解“快照”两个字。所谓“快照”,指我们为容器创建迭代器的时候,相当于给容器拍了一张快照(Snapshot)。之后即便我们增删容器中的元素,快照中的元素并不会做相应的改动。而迭代器遍历的对象是快照而非容器,这样就避免了在使用迭代器遍历的过程中,增删容器中的元素,导致的不可预期的结果或者报错。 +3. 接下来,我举一个例子来解释一下上面这段话。具体的代码如下所示。容器 list 中初始存储了 3、8、2 三个元素。尽管在创建迭代器 iter1 之后,容器 list 删除了元素 3,只剩下 8、2 两个元素,但是,通过 iter1 遍历的对象是快照,而非容器 list 本身。所以,遍历的结果仍然是 3、8、2。同理,iter2、iter3 也是在各自的快照上遍历,输出的结果如代码中注释所示。 + +```java +public static void main(String[] args) { + List list = new ArrayList<>(); + list.add(3); + list.add(8); + list.add(2); + + Iterator iter1 = list.iterator(); // snapshot: 3, 8, 2 + list.remove(new Integer(2)); // list:3, 8 + Iterator iter2 = list.iterator(); // snapshot: 3, 8 + list.remove(new Integer(3)); // list:8 + Iterator iter3 = list.iterator(); // snapshot: 3 + // 输出结果:3 8 2 + + while (iter1.hasNext()) { + System.out.print(iter1.next() + " "); + } + + System.out.println(); + // 输出结果:3 8 + + while (iter2.hasNext()) { + System.out.print(iter1.next() + " "); + } + + System.out.println(); + + // 输出结果:8 + + while (iter3.hasNext()) { + System.out.print(iter1.next() + " "); + } + System.out.println(); +} +``` + +4. 如果由你来实现上面的功能,你会如何来做呢?下面是针对这个功能需求的骨架代码,其中包含 ArrayList、SnapshotArrayIterator 两个类。对于这两个类,我只定义了必须的几个关键接口,完整的代码实现我并没有给出。你可以试着去完善一下,然后再看我下面的讲解。 + +```java +public class ArrayList implements List { + // TODO: 成员变量、私有函数等随便你定义 + @Override + public void add(E obj) { + // TODO: 由你来完善 + } + + @Override + public void remove(E obj) { + // TODO: 由你来完善 + } + + @Override + public Iterator iterator() { + return new SnapshotArrayIterator(this); + } +} + +public class SnapshotArrayIterator implements Iterator { + // TODO: 成员变量、私有函数等随便你定义 + @Override + public boolean hasNext() { + // TODO: 由你来完善 + } + + @Override + public E next() { // 返回当前元素,并且游标后移一位 + // TODO: 由你来完善 + } +} +``` + +### 解决方案一 + +我们先来看最简单的一种解决办法。在迭代器类中定义一个成员变量 snapshot 来存储快照。每当创建迭代器的时候,都拷贝一份容器中的元素到快照中,后续的遍历操作都基于这个迭代器自己持有的快照来进行。具体的代码实现如下所示: + +```java +public class SnapshotArrayIterator implements Iterator { + private int cursor; + private ArrayList snapshot; + + public SnapshotArrayIterator(ArrayList arrayList) { + this.cursor = 0; + this.snapshot = new ArrayList<>(); + this.snapshot.addAll(arrayList); + } + + @Override + public boolean hasNext() { + return cursor < snapshot.size(); + } + + @Override + public E next() { + E currentItem = snapshot.get(cursor); + cursor++; + return currentItem; + } +} +``` + +这个解决方案虽然简单,但代价也有点高。每次创建迭代器的时候,都要拷贝一份数据到快照中,会增加内存的消耗。如果一个容器同时有多个迭代器在遍历元素,就会导致数据在内存中重复存储多份。不过,庆幸的是,Java 中的拷贝属于浅拷贝,也就是说,容器中的对象并非真的拷贝了多份,而只是拷贝了对象的引用而已。那有没有什么方法,既可以支持快照,又不需要拷贝容器呢? + + + +### 解决方案二 + +1. 我们再来看第二种解决方案。 +2. 我们可以在容器中,为每个元素保存两个时间戳,一个是添加时间戳 addTimestamp,一个是删除时间戳 delTimestamp。当元素被加入到集合中的时候,我们将 addTimestamp 设置为当前时间,将 delTimestamp 设置成最大长整型值(Long.MAX_VALUE)。当元素被删除时,我们将 delTimestamp 更新为当前时间,表示已经被删除。 +3. 注意,这里只是标记删除,而非真正将它从容器中删除。 +4. 同时,每个迭代器也保存一个迭代器创建时间戳 snapshotTimestamp,也就是迭代器对应的快照的创建时间戳。当使用迭代器来遍历容器的时候,只有满足 addTimestampsnapshotTimestamp,说明元素在创建了迭代器之后才加入的,不属于这个迭代器的快照;如果元素的 delTimestamp implements List { + private static final int DEFAULT_CAPACITY = 10; + + private int actualSize; // 不包含标记删除元素 + private int totalSize; // 包含标记删除元素 + + private Object[] elements; + private long[] addTimestamps; + private long[] delTimestamps; + + public ArrayList() { + this.elements = new Object[DEFAULT_CAPACITY]; + this.addTimestamps = new long[DEFAULT_CAPACITY]; + this.delTimestamps = new long[DEFAULT_CAPACITY]; + this.totalSize = 0; + this.actualSize = 0; + } + + @Override + public void add(E obj) { + elements[totalSize] = obj; + addTimestamps[totalSize] = System.currentTimeMillis(); + delTimestamps[totalSize] = Long.MAX_VALUE; + totalSize++; + actualSize++; + } + + @Override + public void remove(E obj) { + for (int i = 0; i < totalSize; ++i) { + if (elements[i].equals(obj)) { + delTimestamps[i] = System.currentTimeMillis(); + actualSize--; + } + } + } + + public int actualSize() { + return this.actualSize; + } + + public int totalSize() { + return this.totalSize; + } + + public E get(int i) { + if (i >= totalSize) { + throw new IndexOutOfBoundsException(); + } + return (E) elements[i]; + } + + public long getAddTimestamp(int i) { + if (i >= totalSize) { + throw new IndexOutOfBoundsException(); + } + return addTimestamps[i]; + } + + public long getDelTimestamp(int i) { + if (i >= totalSize) { + throw new IndexOutOfBoundsException(); + } + return delTimestamps[i]; + } +} + +public class SnapshotArrayIterator implements Iterator { + private long snapshotTimestamp; + private int cursorInAll; // 在整个容器中的下标,而非快照中的下标 + private int leftCount; // 快照中还有几个元素未被遍历 + private ArrayList arrayList; + + public SnapshotArrayIterator(ArrayList arrayList) { + this.snapshotTimestamp = System.currentTimeMillis(); + this.cursorInAll = 0; + this.leftCount = arrayList.actualSize(); + this.arrayList = arrayList; + + justNext(); // 先跳到这个迭代器快照的第一个元素 + } + + @Override + public boolean hasNext() { + return this.leftCount >= 0; // 注意是>=, 而非> + } + + @Override + public E next() { + E currentItem = arrayList.get(cursorInAll); + justNext(); + return currentItem; + } + + private void justNext() { + while (cursorInAll < arrayList.totalSize()) { + long addTimestamp = arrayList.getAddTimestamp(cursorInAll); + long delTimestamp = arrayList.getDelTimestamp(cursorInAll); + if (snapshotTimestamp > addTimestamp && snapshotTimestamp < delTimestamp) { + leftCount--; + break; + } + cursorInAll++; + } + } +} +``` + +7. 实际上,上面的解决方案相当于解决了一个问题,又引入了另外一个问题。ArrayList 底层依赖数组这种数据结构,原本可以支持快速的随机访问,在 O(1) 时间复杂度内获取下标为 i 的元素,但现在,删除数据并非真正的删除,只是通过时间戳来标记删除,这就导致无法支持按照下标快速随机访问了。数组随机访问这块知识点,我就不展开讲解了。 +8. 现在,我们来看怎么解决这个问题:让容器既支持快照遍历,又支持随机访问? +9. 解决的方法也不难,我稍微提示一下。我们可以在 ArrayList 中存储两个数组。一个支持标记删除的,用来实现快照遍历功能;一个不支持标记删除的(也就是将要删除的数据直接从数组中移除),用来支持随机访问。对应的代码我这里就不给出了,感兴趣的话你可以自己实现一下。 + + + + + + +