Design-Pattern-CoR

总算到了行为模式。回顾前文,在创建型模式中,关注如何创建某类实例,在结构型模式中,关注将不同类和对象组装到一起,以扩展或适配功能。而在行为模式中,关注对象间的交互和职责划分。

首先介绍责任链模式(Chain of Responsibility, CoR责任链模式允许将请求沿着处理链传递,直到有一个处理器能够处理它。每个处理器决定是否处理请求或将其传递给链中的下一个处理器。

场景

在业务代码执行前,通常会进行多种校验,例如权限校验、参数校验、状态校验、缓存校验等。这些校验可能在不同业务中以不同组合,不同顺序的形式存在。如果在每个业务类中都复制粘贴这些校验并调整顺序,会导致代码重复率大。相对简洁一点的做法是把这些校验抽取成方法,这样在每个业务中只是调用这些校验方法。校验方法通常有个特点,前面的校验不通过,后面的校验则没必要继续了。随着业务复杂,校验方法越来越多。方法之间互相耦合,修改某个部分可能会影响到其他部分。

责任链模式

如果把校验过程进行抽象,可以提取出处理器(handler)。处理器的功能是对请求进行操作,并根据操作结果决定是由下一个处理器继续处理,还是直接终止后续的所有处理。处理器通过成员变量持有下一个处理器的引用,多个处理器通过链表的形式组合在一起。链表上的顺序决定了处理器执行校验的顺序。

如此一来,每个校验都可以重构成一个处理器。而要使得处理器能通过链表一样串起来,这需要所有处理器是同一个类型,例如实现了同一个接口,或继承自同一个基类。这样的结构在装饰模式中也已经见过了。在java中,优先会使用接口来表示多个类的共同行为,因此定义处理器接口handler。而多个处理器实现类中,有很多共有部分,例如都需要持有下一个处理器的成员变量,成员变量的设置方法,以及链式调用通用逻辑。因此会使用一个抽象基类实现处理器接口,把这些共同的部分定义在基类中,这个抽象基类称为基础处理器(base handler)。而具体处理器(Concrete Handlers),通过继承基础处理器,实现抽象方法,来添加特有的具体处理逻辑。

基于上述设计思路,可以给出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 处理器接口
interface Handler {
void handle(Request request);
void setNext(Handler handler);
}

// 基础处理器
abstract class BaseHandler implements Handler {
protected Handler nextHandler;

public void setNext(Handler nextHandler) {
this.nextHandler = nextHandler;
}

public abstract void handle(Request request);
}

// 具体处理器A
class ConcreteHandlerA extends BaseHandler {
@Override
public void handle(Request request) {
if (request.getType().equals("TypeA")) {
System.out.println("ConcreteHandlerA 处理请求: " + request.getContent());
} else if (nextHandler != null) {
nextHandler.handle(request); // 传递给下一个处理器
}
}
}

// 具体处理器B
class ConcreteHandlerB extends BaseHandler {
@Override
public void handle(Request request) {
if (request.getType().equals("TypeB")) {
System.out.println("ConcreteHandlerB 处理请求: " + request.getContent());
} else if (nextHandler != null) {
nextHandler.handle(request);
}
}
}

// 请求类
class Request {
private String type;
private String content;

public Request(String type, String content) {
this.type = type;
this.content = content;
}

// Getter方法...
}

// 客户端
public class Client {
public static void main(String[] args) {
Handler handlerA = new ConcreteHandlerA();
Handler handlerB = new ConcreteHandlerB();
handlerA.setNext(handlerB); // 构建责任链

// 发送请求
handlerA.handle(new Request("TypeB", "请求内容"));
}
}
classDiagram
    direction RL
    class Client
    
    class Handler {
        << interface >>
    	+ Handler()
    	+ setNext()
    }
	Client --> Handler
	
	class ConcreteHandler {
		+Handler()
	}
    ConcreteHandler ..|> Handler
	
	class BaseHandler {
		<<abstract>>
		- Handler handler
    	+ Handler()*
    	+ setNext()
	}
	BaseHandler ..|> Handler
	BaseHandler ..o Handler

	ConcreteHandler --|> BaseHandler
    

责任链变体

谈到责任链的结构,总感觉和实际使用情况不符。对比上述关于校验流程的例子和示例代码,会发现也是匹配不上的。这是因为责任链模式存在很多变体,实际使用的是各种变体。在此简单介绍最常见的变体。

  • 中断式责任链:拿校验的例子来说,校验过程通常是一个接着一个执行,若任意一个校验不通过,则立即终止后续的校验。上述示例代码显然不满足这个要求。因为需要根据校验结果判断是否继续校验,那么校验方法需要提供能表示是否校验通过的标识,例如返回一个boolean类型,true表示通过校验。如此可以对handle方法改造。示例代码如下,调整了基础处理器handle的方法,并定义了实际执行校验逻辑的validate抽象方法。任意一个处理器校验失败,就会直接return,结束链式调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 短路型责任链
abstract class BaseHandler implements Handler {
protected Handler nextHandler;

public void setNext(Handler nextHandler) {
this.nextHandler = nextHandler;
}

public boolean handle(Request request) {
if (!validate(request)) return false;
return nextHandler == null || nextHandler.handle(request);
}

public abstract boolean validate(Request request);
}
  • 全传播责任链:与中断式责任链相反,全传播责任链会将请求一直传递,必须遍历所有处理器。请求可能被多个处理器处理,也可能没有处理器处理。
  • 纯责任链:请求必须被某个处理器处理,且每个处理器只能选择处理或传递给下一节点(不可既处理又传递)。
  • 不纯责任链:允许处理器部分处理请求后继续传递(如拦截器模式),或所有处理器均尝试处理(如中间件管道)。
  • 动态责任链:链的节点可在运行时动态增删或重新排序。

这些变体不全是互斥的,例如spring mcv的拦截器,在上述变体中属于不纯的、可中断、动态责任链。

与装饰模式的区别

对比装饰模式与责任链模式的结构,会发现两者一模一样,概念也几乎一一对应,并且两者都依赖递归组合将需要执行的操作传递给一系列对象。

有些文章提到装饰模式无法中断请求传递。但这不是明显差别,因为全传播责任链会要求请求遍历所有处理器,不会在调用链中间中断。也有文章提到从语义上说,装饰模式强调给对象动态添加功能,责任链强调众多处理器之一对对象进行处理。但不纯责任链允许多个处理器处理请求。我们也可以将这多个处理器,理解为是对功能的动态添加。例如spring mvc中每添加一个拦截器,都是对拦截器整体功能的扩展。客户端使用的处理器,其实是所有处理器功能的结合。使用装饰模式的语义看待责任链一点也不违和。另外处理器判断某个请求是否是其职责,是否该处理,这判断本身也是一种处理。也就是说在这种理解下,就算处理器把处理委派给了下一个处理器,其本身也完成了一部分处理,不能很严谨地说只进行传递。装饰模式也能符合责任链的语义,例如数据加密的装饰器,可以判断数据是否已经加密,如果已经加密则不处理,并委派给下一个装饰器。在这种场景下,也并不是所有的装饰器都会对请求进行处理,装饰器也可以选择处理或传递中的一个。

有些文章认为责任链是装饰器的一种特殊形式,笔者赞同这个观点。如果硬是要比较两者的差别,笔者认为主要区别在于责任链模式的每个处理器,是相对独立的,彼此间互相不知道。而装饰模式中的装饰器之间,耦合程度会更高。例如装饰器之间可能存在依赖,必须按照特定顺序添加装饰器。 并且装饰模式中会有一个专门位于链尾部的具体部件,而在责任链模式中,每个处理器并不知道自己是否位于尾部。

一些想法

回到实际,我们似乎很少在代码中通过责任链去执行校验,上文把校验过程改造为责任链的例子并不好。这是因为不同的校验所需要的入参/上下文不同。例如校验方法的入参可能是日期,可能是金额,亦或各种字段可能的组合。这意味着不同的校验方法,应对的不是同一个请求,也就无法抽取出公共的处理抽象方法。这当然也有解决方法,例如新建一个自定义类,各种校验需要的入参都被包含在其中,类名就叫ValidationContext。但这也加重了客户端的负担,需要收集各个字段,负责这个实例的创建和初始化,这可能得不偿失。

因此,实际适合责任链模式的场景,都需要多个处理器处理的是同一个请求。常被拿来举例子的权限审批,不同职级是对同一个请求表单进行审查。spring mcv中,不同拦截器是对同一个HTTP请求和响应进行处理和拦截。所以在考虑把一连串相似的处理重构成责任链时,还需要考虑这些处理应对的是否为同一个请求,或者需要衡量改造成同一个请求的成本。

此外,多个处理器是否有必要通过链表串在一起,也是个值得思考的问题。备选方案是可以建一个List包含所有处理器,客户端通过for循环,按顺序依次调用所有处理器。在for循环中也可以通过ifbreak关键字来跳出循环,模拟责任链中任意处理器中断请求传递的过程。对于客户端来说,只知道请求由一个处理器集合负责处理,不必关心其中的顺序,具体由谁来执行。List也支持动态添加、删除元素的操作。整体看使用List和责任链的行为和特征是一致的。

当处理器的功能不复杂,这么做确实可行。但当处理器需要在所有剩下处理器处理完后再执行某些操作,简单的List循环就不再能满足需求。责任链的递归调用模式可以产生栈的效果,每个处理器可以在入栈和出栈时执行操作,也就是预处理和后处理。此外,在一些场景中,处理器天然地会以树的形式组织在一起。常被拿来举例的是UI亦或是DOM。我们点击了其中某个元素,产生的点击事件会沿着元素一直追溯到树的根,沿途的节点所代表的处理器都可能会处理这个事件,也可能传递给下一个节点。在这种场景下,责任链的结构天然地比List更合适。

总结

责任链模式以链表结构串起一系列类似的处理器。请求会沿着链表传递,每个处理器都可以决定自己是否处理,是否交由下一个处理器处理,是否中断处理传递。

责任链模式的角色包括处理器接口、基础处理器、具体处理器。

责任链模式存在不同变体,主要的大类包括中断/全传播责任链,纯/不纯责任链,动态责任链等。

责任链模式和装饰模式几乎一模一样。笔者认为责任链模式是装饰模式的一种特例。

责任链模式适合的场景需要多个处理器处理的是同一个请求。

处理器以树结构组织时天然地适合责任链模式。有时也可以考虑使用List储存处理器和循环调用作为责任链模式的替代方案。


Design-Pattern-CoR
http://dracoyus.github.io/2025/05/07/Design-Pattern-CoR/
作者
DracoYu
发布于
2025年5月7日
许可协议