责任链、领域模型和事务的恩怨情仇

  责任链模式是一种非常经典的行为型设计模式,本身比较简单,但在真实开发中,我们需要考虑领域模型,需要考虑事务,就会变得复杂起来。

  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个方面都可能给开发人员带来不小的工作量:

  复杂的业务特性跟领域模型的配合对事务的处理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值