责任链模式是一种非常经典的行为型设计模式,本身比较简单,但在真实开发中,我们需要考虑领域模型,需要考虑事务,就会变得复杂起来。
1 初识责任链
「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.
❞
翻译过来就是,把发送者和接收者解耦,把所有接收者放在一条链上,让请求沿着这个链传递直到有一个节点处理成功。
不过,在实际开发中,这个定义多多少少有点过时了。有些场景做了改变,那就是每个节点都会处理这个请求,一直到所有节点都处理完毕,其中一个节点处理失败,就终止。
1.1 代码实现
来假设一个场景,一个电商系统要处理一笔购买请求,需要3个步骤,保存订单、账户扣款、扣减库存。我们先定义一个UML
类图:
上图定义了一个IProcessor
接口,然后对订单、账户和库存分别定义了实现,这3个实现被组织成了一个责任链,入参是Apply
对象,即购买请求。责任链逻辑在ProcessorChain
类,代码如下
public class ProcessorChain { private List<IProcessor> processors = new ArrayList<>(3); public void addProcessor(IProcessor processor){ processors.add(processor); } public void doProcess(Apply apply){ processors.forEach(r -> r.process(apply)); } }
1.2 优点
1.2.1
使用责任链模式可以将复杂业务拆分成小的单元,让不同的开发人员关注小的单元业务。
❝试想如果上面订单、账户和库存在一个类里面,业务代码会非常复杂和庞大,开发人员分工协作也不太容易。
❞
1.2.2
如果要增加一个节点,不用对前面的节点进行修改,符合开闭原则。
❝责任链模式的使用非常广泛,比如
❞mybatis
中的Interceptor
,xxljob
的子任务调度等。
2 领域模型
上面责任链代码的实现有点太简单了,如果我们引入领域模型,要怎么处理呢?
❝领域模型是
❞DDD
中的概念,是指针对业务领域里中关键模块进行抽象,构建出软件系统中的映射模型。
比如,这里为了让业务更加清晰,我们定义了一个购物的领域模型,ShoppingModel。代码如下:
public class ShoppingModel { private Apply apply; private OrderDo orderDo; private AccountDo accountDo; private StorageDo storageDo; public ShoppingModel(Apply apply){ this.apply = apply; } }
ShoppingModel
里面定义了订单、库存和账户的Do
模型。这时上一节定义的UML
类图需要修改成如下:
这时ProcessorChain
的代码变成了下面这样:
public class ProcessorChain1 { private List<IProcessor> processors = new ArrayList<>(3); public void addProcess(IProcessor processor){ processors.add(processor); } public void doProcess(Apply apply){ ShoppingModel shoppingModel = new ShoppingModel(apply); processors.forEach(r -> r.process(shoppingModel)); } }
我们抽象出了Do
模型,下一步就要考虑DML
操作了,这不得不考虑事务处理。
3 事务登场
引入领域模型后,在订单节点处理完成后,生成orderDo
,账户节点处理完成后,生成accountDo
,库存节点处理完成后生成storageDo
。
这3个Do
都需要持久化到数据库,而且必须是一个原子操作,那要怎么处理呢?
3.1 doProcess方法加事务
最简单的就是在ProcessorChain
的doProcess
方法上加@Transactional
注解,这样整个流程就在一个事务里面了。
这时问题来了,如果3个processor
都有非常耗时的处理逻辑,整个事务就耗时太长了,长时间占用数据库连接资源不能释放。
3.2 加一个db节点
如果在责任链最后面加一个db
节点DbProcessor
,前面3个节点负责处理业务逻辑,DbProcessor
节点负责持久化数据,这样整个事务就除去了业务处理花费的时间。
DbProcessor类中process代码如下:
@Transactional @Override public boolean process(ShoppingModel shoppingModel) { orderMapper.save(shoppingModel.getOrderDo()); accountMapper.update(shoppingModel.getAccountDo()); storageMapper.update(shoppingModel.getStorageDo()); return true; }
4 新的问题
但是又有新的问题,由于业务的需要,必须要在StorageProcessor
节点扣减库存并落库。
如果我们在StorageProcessor
的process
方法上加上@Transactional
注解,并不能保证事务传播到DbProcessor,这就需要再次改造了。
我们使用责任链模式的第二种写法进行改造,UML
类图如下:
从图中我们可以看到,在4
个processor
中,前面的processor
增加了对后面processor
的依赖。每个processor
都需要一个指向next
节点的引用。每次process
处理结束后,判断一下next
是否为空,如果next
不为空,则调用next.process
方法。
为了让代码更加简洁,我们需要作出3个修改。
4.1 引入了模板模式
模板类是AbstractProcessor
,它的process
方法来实现所有Processor
的公共逻辑,代码如下:
public abstract class AbstractProcessor implements IProcessor { protected IProcessor next; @Override public boolean process(ShoppingModel shoppingModel){ boolean result = doProcess(shoppingModel); if (result && null != next){ return next.process(shoppingModel); } return result; } public void setNext(IProcessor next){ this.next = next; } protected abstract boolean doProcess(ShoppingModel shoppingModel); }
这样每个节点类就不需要实现IProcessor
的process
方法,而是继承AbstractProcessor
,实现抽象方法doProcess
。
4.2 ProcessorChain类
Processor
类不能放在List
上了,而要组织成一个链表,代码如下:
public class ProcessorChain2 { private IProcessor head; private IProcessor tail; public ProcessorChain2(IProcessor head){ this.head = head; this.tail = head; } public void addProcess(IProcessor processor){ tail.setNext(processor); this.tail = processor; } public void doProcess(ShoppingModel shoppingMode){ head.process(shoppingMode); } }
4.3 StorageProcessor控制事务
事务需要由StorageProcessor
来控制,代码如下:
public class StorageProcessor extends AbstractProcessor { @Transactional @Override protected boolean doProcess(ShoppingModel shoppingModel) { storageMapper.update(shoppingModel.getStorageDo()); return true; } }
5 一些思考
5.1 领域模型
引入领域模型,把复杂业务抽象成程序实体,简化了业务,代码结构也更加清晰。并且DB
节点的加入,让事务后移,减去了计算过程花费的时间。
同时,引入领域模型也会带来一些问题,比如在上面的电商购物案例中,如果每个节点都必须要有DML
操作,是否还需要抽象出ShoppingModel
?
5.2 事务控制
我们肯定是要尽可能地降低事务的耗时。除了优化sql
,程序中的处理时间也是一个优化点。是否要加DbProcessor
节点,我们考虑下面几点:
- 前面节点的计算过程是否耗时很多
- 前面的节点是否需要
DML
操作 - 事务控制加在什么地方
5.3 批量执行
如果又来一个改造,把电商系统收到的请求都缓存下来,之后批处理,又该怎么做呢?
- 循环调用
ProcessorChain
的doProcess方法
。
❝业务代码改动小,但是跟数据库交互多。
❞
- 去掉
DB
节点,其他3
个节点各自管理事务,事务批量提交。
❝改造大,需要为每个
❞Apply
记录3
个处理状态。好处是跟数据库交互少,3
个节点可以并行执行。
类似下面伪代码:
public class OrderProcessor extends AbstractProcessor { @Resource private OrderMapper orderMapper; @Transactional @Override protected boolean doProcess(List<Apply> Applys) { orderMapper.save(apply2Order(Applys)); return true; } private List<Order> apply2Order(List<Apply> Applys){ List<Order> orders = null; //... return orders; } }
5.4 公共依赖
回到领域模型,如果我们不考虑批量,前面3
个节点也不做DML
操作,那引入一个DB
节点确实是非常好的设计。
可是项目上线若干个月后,团队遇到一个问题,OrderProcessor
必须引入一个公共组件,这个组件里面有DML
操作。这样系统又是一个不小的改造。
6 总结
6.1
责任链模式在我们开发中使用非常多,要学会这种模式也非常容易。
6.2
在我们实际的开发过程中,用好责任链并不简单,因为我们不能脱离实际业务去考虑模式本身,下面5个方面都可能给开发人员带来不小的工作量:
- 复杂的业务特性
- 跟领域模型的配合
- 对事务的处理
- 后期需求变更
- 公共组件引入
所以,在详细设计阶段,做好业务梳理和抽象是至关重要的。
6.3
从架构师角度讲,后期要更改框架并不难,就跟我这篇文章介绍思路类似。难的是面对复杂的业务,变动框架带来的犄角旮旯的小问题,要解决这些小问题,一线操刀的程序员压力并不会小。