观察者模式在事件驱动中的运用

发布时间 2023-09-23 12:36:31作者: ShawnFux
title: 观察者模式在事件驱动中的运用
categories:
  - Java
tags:
  - 设计模式
abbrlink: 53cb85e5
date: 2023-03-02 15:06:28

在面对一些较为复杂的业务时,合理的使用事件驱动设计,能够帮助我们对主业务逻辑和分支业务逻辑进行解耦,更好的实现开闭原则。

什么是观察者模式?

观察者模式的实现是基于一种发布订阅的机制来实现的,通常来说会有两种角色,发布者和感兴趣的订阅者,比方说有个卖衣服的店有件衣服的款式很火爆,很多人都前来购买,但是没货了,于是店家就让顾客留下电话号码,说等来货了我再打电话通知你们,终于这款爆款衣服到货了,店家准备打电话通知顾客来购买,到这里你可能觉得很完美。但是随着店家的生意越来越好,经常出现缺货,留下电话等货的顾客越来越多,到最后店家几乎不能做其它事情,只能不停的打电话通知顾客,非常的忙!店家心想这可不行,这样生意没法做了啊,全部把时间浪费在了打电话通知顾客取货的事情上,于是店家想了一个很棒的想法,他创建了一个好友群,然后让想要购买某件爆款衣服的顾客加入这个群,等到衣服到货时,他只需要在对应的群发一条衣服到货了的信息就行了,不需要再挨个给每个顾客打电话了。在这个故事里,发布者就是店家,而订阅者就是顾客,顾客们对那件衣服感兴趣就加入对应的群,等待群里的到货通知再做一步行动。

使用事件对复杂业务解耦

初版代码

接下来以一个常被举例的业务逻辑 创建订单 这个业务场景为例子来说明,如何使用事件来对复杂业务逻辑进行解耦。

假设你有一个 OrderApplicationService 服务,并且对外提供 createOrder 方法来创建订单:

public class OrderApplicationService {

    private final StockService stockService;
    private final OrderRepository orderRepository;
    private final BuyerCenterService buyerCenterService;
    private final SellerCenterService sellerCenterService;
    
    @Transactional
    public void createOrder(Order order) {
        // 1.校验订单信息数据
        verify(order);
        // 2.扣减库存
        stockService.deduction(order);
        // 3.保存订单信息
        orderRepository.save(order);
        // 4.给用户发送订单创建成功站内信息通知
        buyerCenterService.sendNew(order);
        // 5.发送站内信息通知商家处理订单
        sellerCenterService.sendNews(order);
    }
}

在创建订单的时候,需要对订单数据合法性校验然后扣减商品库存保存订单信息,最后发送站内信息通知买家(用户)和卖家(商家)订单处理结果即可。最开始的业务需求较为简单这样写好像没什么问题,随着版本迭代业务需求变更,现在提出新的需求,需要在创建订单成功时,再发送手机短信通知买家,然后还要根据这笔订单给用户增加积分,那么你的创建订单方法很有可能就会变成下面这样:

public class OrderApplicationService {

    private final StockService stockService;
    private final OrderRepository orderRepository;
    private final BuyerCenterService buyerCenterService;
    private final SellerCenterService sellerCenterService;

    @Transactional
    public void createOrder(Order order) {
        // 1.校验订单信息数据
        verify(order);
        // 2.扣减库存
        stockService.deduction(order);
        // 3.保存订单信息
        orderRepository.save(order);
        // 4.给用户发送订单创建成功站内信息通知
        buyerCenterService.sendNew(order);
        // 5.发送站内信息通知商家处理订单
        sellerCenterService.sendNews(order);
        // 6.给用户发送订单创建成功手机短信通知
        buyerCenterService.sendSMS(order);
        // 7.给用户增加积分
        buyerCenterService.calculatePoints(order);
        // N.更多需求...
    }
}

好像这样做也没什么问题,功能也能实现,但是随着业务需求的变更,你需要频繁的去修改 createOrder 方法,违背了开闭原则的思想,最终会导致你的 createOrder 方法非常膨胀,失去了职责单一的初衷,它的业务语义越来越模糊,而且 createOrder 方法包含了太多分支逻辑比如发短信通知,增加积分,由于这些操作都在同一个事务,这将会造成任何一个分支逻辑的失败,都会导致整体事务的回滚从而导致创建订单失败,这显然不是我们愿意见到的,我们不希望用户增加积分失败这样的分支逻辑,却导致订单都创建失败。所以我们迫切需要将主逻辑与分支逻辑分离开来,接下来就轮到 事件 登场来对我们的主逻辑和分支业务逻辑进行解耦。

使用事件改造

public class OrderApplicationService {

    private final StockService stockService;
    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher publisher;

    @Transactional
    public void createOrder(Order order) {
        // 1.校验订单信息数据
        verify(order);
        // 2.扣减库存
        stockService.deduction(order);
        // 3.保存订单信息
        orderRepository.save(order);
        // 4.发布订单创建成功事件
        publisher.publishEvent(new OrderCreatedEvent(this,order));
    }
}

// 订单创建成功事件对象
public class OrderCreatedEvent extends ApplicationEvent {

    // 包含了本次事件的主体订单对象
    private final Order order;

    public OrderCreatedEvent(Object source,Order order) {
        super(source);
        this.order = order;
    }

    public Order getOrder() {
        return order;
    }
}

通过改造createOrder这个方法,我们将主逻辑校验订单,扣减库存,持久化保存订单信息保留下来,最后借助 Spring 框架提供的事件机制,没有再额外自己再去实现一套订阅发布的观察者模式,在执行完创建订单最后一个主逻辑时,发布了一个 orderCreated 事件,那么刚才剩下的发送短信和给用户加积分的分支逻辑怎么去调用执行了?

public class BuyerCenterService {
    public void calculatePoints(Order order) {
        // TODO
    }

    @EventListener(OrderCreatedEvent.class)
    public void orderCreatedEvent(OrderCreatedEvent event) {
        // 拿到创建成功的订单对象
        Order order = event.getOrder();
        // 给用户计算增加积分
        calculatePoints(order);
    }
}

这里以增加用户积分这一个分支逻辑为例,其它的分支逻辑同理,都是监听订单创建成功事件做相应的逻辑处理即可,通过事件机制,我们就实现了主逻辑和分支业务逻辑的解耦,这样 createOrder 方法只需要专注于做好处理创建订单这一件事,避免一个方法功能业务知识过多。但是这样做还不能算真正的完全解耦,虽然我们把给用户增加积分这个分支逻辑剥离出去了,但是它还是属于 createOrder 这个方法的一部分,calculatePoints 方法的成功或失败还是会影响到创建订单的主逻辑事务,因为他们还是同步调用的,所以为了更进一步的实现解耦,需要使用异步事务。

异步事件

如果你是在 Spring 的环境下,那么通常实现异步很简单,你只需要在需要异步执行的方法添加一个 @Async 注解即可:

public class BuyerCenterService {

    public void calculatePoints(Order order) {
        // TODO
    }

    @Async // 添加注解实现异步
    @EventListener(OrderCreatedEvent.class)
    public void orderCreatedEvent(OrderCreatedEvent event) {
        // 拿到创建成功的订单对象
        Order order = event.getOrder();
        // 给用户计算增加积分
        calculatePoints(order);
    }
}

由于开启了异步,所以给用户增加积分的方法将不一定等待 createOrder 主逻辑正确执行完后再执行,也就意味着有可能创建订单其实失败了,但是你的计算增加积分却执成功了,这显然不是我们所期望的,我们期望的是只有当主逻辑正确执行成功了,分支逻辑才能被执行,这其中是有着先后顺序的依赖关系的。那么如何实现了?

TransactionalEventListener

public class BuyerCenterService {

    public void calculatePoints(Order order) {
        // TODO
    }

    @Async
    @TransactionalEventListener(value = OrderCreatedEvent.class, 
                                phase = TransactionPhase.AFTER_COMMIT)
    public void orderCreatedEvent(OrderCreatedEvent event) {
        // 拿到创建成功的订单对象
        Order order = event.getOrder();
        // 给用户计算增加积分
        calculatePoints(order);
    }
}

在 Spring 框架里面还额外提供了一个 @TransactionalEventListener 注解,它也有 @EventListener 同样的功能,但是在此基础还专门为解决事务之间的事件提供了一些额外的机制,其中该注解有一个 phase 参数,用来控制该监听方法何时执行:

public @interface TransactionalEventListener {
   // 控制监听器方法何时执行,默认值在事务提交成功后执行
   // 另外还有:
   // BEFORE_COMMIT 事务提交之前执行
   // AFTER_ROLLBACK 事务回滚之后执行
   // AFTER_COMPLETION 事务无论是回滚或提交都会执行  
   TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;
}

一般来说我们使用默认的 AFTER_COMMIT 事务提交成功后执行,就能达到我们刚才的需求,就能保证创建订单主逻辑能够正确执行成功,其它的异步分支业务逻辑才会执行。

结语

通过分析创建订单的业务,我们从最开始的所有业务逻辑都在一个方法处理,到后来通过使用事务进行解耦拆分主逻辑和分支逻辑,到最后通过将分支逻辑异步化更进一步提升主逻辑的响应处理速度。