责任链模式是一种非常经典的行为型设计模式,本身比较简单,但在真实开发中,我们需要考虑领域模型,需要考虑事务,就会变得复杂起来。
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 processors = new ArrayList<>(3);
public void addProcessor(IProcessor processor){
processors.add(processor);
}
public void doProcess(Apply apply){
processors.forEach(r -> rcess(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 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 -> rcess(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不为空,则调用nextcess方法。
为了让代码更加简洁,我们需要作出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 nextcess(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){
headcess(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 Applys) {
orderMapper.save(apply2Order(Applys));
return true;
}
private List apply2Order(List Applys){
List orders = null;
//...
return orders;
}
}
5.4 公共依赖
回到领域模型,如果我们不考虑批量,前面3个节点也不做DML操作,那引入一个DB节点确实是非常好的设计。
可是项目上线若干个月后,团队遇到一个问题,OrderProcessor必须引入一个公共组件,这个组件里面有DML操作。这样系统又是一个不小的改造。
6 总结6.1
责任链模式在我们开发中使用非常多,要学会这种模式也非常容易。
6.2
在我们实际的开发过程中,用好责任链并不简单,因为我们不能脱离实际业务去考虑模式本身,下面5个方面都可能给开发人员带来不小的工作量:
复杂的业务特性跟领域模型的配合对事务的处理