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

2022-08-23 16:48:38 浏览数 (1)

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

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类,代码如下

代码语言:javascript复制
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中的Interceptorxxljob的子任务调度等。 ❞

2 领域模型

上面责任链代码的实现有点太简单了,如果我们引入领域模型,要怎么处理呢?

❝领域模型是DDD中的概念,是指针对业务领域里中关键模块进行抽象,构建出软件系统中的映射模型。 ❞

比如,这里为了让业务更加清晰,我们定义了一个购物的领域模型,ShoppingModel。代码如下:

代码语言:javascript复制
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的代码变成了下面这样:

代码语言:javascript复制
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方法加事务

最简单的就是在ProcessorChaindoProcess方法上加@Transactional注解,这样整个流程就在一个事务里面了。

这时问题来了,如果3个processor都有非常耗时的处理逻辑,整个事务就耗时太长了,长时间占用数据库连接资源不能释放。

3.2 加一个db节点

如果在责任链最后面加一个db节点DbProcessor,前面3个节点负责处理业务逻辑,DbProcessor节点负责持久化数据,这样整个事务就除去了业务处理花费的时间。

DbProcessor类中process代码如下:

代码语言:javascript复制
@Transactional
@Override
public boolean process(ShoppingModel shoppingModel) {
    orderMapper.save(shoppingModel.getOrderDo());
    accountMapper.update(shoppingModel.getAccountDo());
    storageMapper.update(shoppingModel.getStorageDo());
    return true;
}

4 新的问题

但是又有新的问题,由于业务的需要,必须要在StorageProcessor节点扣减库存并落库。

如果我们在StorageProcessorprocess方法上加上@Transactional注解,并不能保证事务传播到DbProcessor,这就需要再次改造了。

我们使用责任链模式的第二种写法进行改造,UML类图如下:

从图中我们可以看到,在4processor中,前面的processor增加了对后面processor的依赖。每个processor都需要一个指向next节点的引用。每次process处理结束后,判断一下next是否为空,如果next不为空,则调用next.process方法。

为了让代码更加简洁,我们需要作出3个修改。

4.1 引入了模板模式

模板类是AbstractProcessor,它的process方法来实现所有Processor的公共逻辑,代码如下:

代码语言:javascript复制
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);
}

这样每个节点类就不需要实现IProcessorprocess方法,而是继承AbstractProcessor,实现抽象方法doProcess

4.2 ProcessorChain类

Processor类不能放在List上了,而要组织成一个链表,代码如下:

代码语言:javascript复制
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来控制,代码如下:

代码语言:javascript复制
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 批量执行

如果又来一个改造,把电商系统收到的请求都缓存下来,之后批处理,又该怎么做呢?

  • 循环调用ProcessorChaindoProcess方法

❝业务代码改动小,但是跟数据库交互多。 ❞

  • 去掉DB节点,其他3个节点各自管理事务,事务批量提交。

❝改造大,需要为每个Apply记录3个处理状态。好处是跟数据库交互少,3个节点可以并行执行。 ❞

类似下面伪代码:

代码语言:javascript复制
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

从架构师角度讲,后期要更改框架并不难,就跟我这篇文章介绍思路类似。难的是面对复杂的业务,变动框架带来的犄角旮旯的小问题,要解决这些小问题,一线操刀的程序员压力并不会小。

0 人点赞