DDD落地实现的深水区(5)整洁架构落地(下)

整洁架构的落地实现

大家好,我是范钢老师。上一期我们探讨了整洁架构的代码落地实现,将整洁架构的核心思想,将上层的业务与底层的技术解耦,最终落地到了“数据接入层”、“数据访问层”与“接口层”。数据接入层,通过MVC框架,将前端UI与后端服务解耦,使得前端可以有各种不同的展现方式(如Web、客户端、移动端APP、物联网设备,等等),但后端服务只需要编写一套,大大降低了开发与维护成本;数据访问层,通过DAO接口及其各种不同的实现类,可以实现各种方式的数据存储(如关系型数据库、NoSQL/NewSQL、数据缓存、文件存储,等等),但上层的业务代码不受影响。整个的设计思路,大家可以仔细研究一下上面这张图。有了这样的设计,上层的业务可以完全按照领域模型的规范编写代码,而完全不用担心数据是如何存储。

然而,上期我对这些设计思想的举例,全部都是Web服务端的案例,是不是只能应用在Web服务端呢?显然不是的,它同样可以应用到嵌入式、桌面端的设计。大家只需要记住,数据接入层接入的是人机交互这一端,数据访问层对接的是数据存储这一端,一切就容易理解了。

DDD落地实现的深水区(5)整洁架构落地(下)_第1张图片

如图所示,在Web应用中,人机交互的可以是浏览器、移动App或微信小程序,它们通过Controller对接,与后端服务交互,因此“数据接入层”是Controller。同理,在桌面应用中,人机交互的是各种窗口。通过在窗口中的操作,后端会响应这些操作(如onButtonClick、onWindowDrag等)。响应事件的消息处理层就是桌面应用的“数据接入层”。以往在软件开发中,会在响应事件后编写大量业务处理代码。这样的设计使得前端UI的变更,与后台业务的变更,掺杂并耦合在一起,不利用日后的维护。现在,按照整洁架构的思想,当响应事件以后什么都别干,就是简单地去调用Service中的相应方法,而将那些复杂的业务都写到Service的这些方法中,最后通过Dao来存储和访问数据库。通过这样的设计,将大大提高代码质量,降低日后变更维护的成本。

除此之外,嵌入式系统是否可以采用整洁架构呢?同样是可以的。嵌入式系统往往是一些底层驱动程序,它们没有人机交互的UI。但是,站在全局的角度来看,人机交互是通过上层的应用程序实现的,而应用程序最终会调用嵌入式的底层驱动。从这个角度来说,嵌入式系统与外部的接口就是“数据接入层”。嵌入式系统要运行在不同的平台中,数据接入的方式可能存在巨大的不同。过去的开发,由于数据接入以后就直接开始编写业务处理程序了,数据接入层与业务代码耦合在一起,使得一旦切换平台,整个嵌入式系统都要重新开发。采用整洁架构的思想,数据接入进来以后什么都不要做,就是去调用后台的Service,让数据接入与业务代码分离。当日后要切换平台时,只需要重新编写一套接入代码,而业务代码不变。有了这样的设计,就为日后的技术不确定性,注入了更多的确定性。

最后是“接口层”的设计,它将整个系统中用到的所有技术框架,与业务代码分离并解耦,从而实现“整洁架构”。所有技术框架都需要在接口层中进行封装,才能开放给上层的业务。上层的业务代码都不能直接调用任何底层的技术框架,而是调用接口层的相应接口,通过接口的实现类来完成对底层技术框架的调用。因此,MVC框架有OrmController和QueryController,数据访问框架有BasicDao及其实现类,缓存有BasicCache及其实现类,这些都是“接口层”设计的具体体现。有了这样的设计,所有的业务都编写在Service, Entity, ValueObject这些业务领域层的代码中,而其它的层次都是技术。业务领域层的设计应当尽量纯洁,尽量避免调用和依赖任何技术,技术都是与接口层的实现类相互耦合。

保持领域层的纯洁,这是整洁架构非常重要的设计思想,然而在实际项目中会遇到诸多的挑战,必须灵活地应对。譬如,在基于微服务的系统中完成用户下单,这个操作不仅仅是让订单微服务去保存订单那么简单,还需要调用客户微服务完成账户的支付,用户库存微服务完成库存的扣减。更加关键的是,以上所有的操作都必须要有一个全局的分布式事务。因此,以上功能以往的设计需要在OrderService中通过注解来引入分布式事务,那么就不得不在Service中引入相应的技术框架,使得领域层的代码不再那么纯洁。

DDD落地实现的深水区(5)整洁架构落地(下)_第2张图片

解决以上问题的解决思路,就是在它们之上再加入一个OrderAggService的聚合服务。这里的聚合服务与DDD中的聚合关系不是一个概念,而是在微服务设计中,将多个原子服务组合起来使用,从而完成一些更加复杂业务的设计模式。当用户下单时,首先调用OrderAggService来完成下单操作。这时,OrderAggService会去调用OrderService创建订单,调用AccountService完成支付,调用InventoryService完成库存扣减,最后由OrderAggService实现分布式事务。这样的设计,其它的原子服务都是纯洁的,只有聚合服务与分布式事务耦合,从而实现技术与业务的分离。

@Service("orderAgg")
@Slf4j
public class OrderAggServiceImpl implements OrderAggService {
    @Autowired
    private OrderService orderService;
    @Autowired
    private AccountService accountService;
    @Autowired
    private InventoryService inventoryService;
    @Override
    public Long placeOrder(@NonNull Order order) {
        log.info("begin the trade... xid: "+ RootContext.getXID());
        Long orderId = orderService.create(order);
        log.debug(String.format("create an order: [orderId: %d]", orderId));
        return orderId;
    }

    @Override
    @GlobalTransactional(name = "seata-group-trade", rollbackFor = Exception.class)
    @Transactional
    public void payoff(@NonNull Order order) {
        if(order.getPayment()==null||order.getPayment().getAccountId()==null)
            throw new ValidException("no account for payoff: [orderId: %s]", order.getId());
        Payment payment = order.getPayment();
        Double balance = accountService.payoff(payment.getAccountId(), payment.getAmount());
        log.debug(String.format("payoff for the order: [orderId:%d,accountId:%d,amount:%f,balance:%f]",
                order.getId(), payment.getAccountId(), payment.getAmount(), balance));
        stockOut(order);
        order.setStatus("PAYOFF");
        order.getPayment().setStatus("PAYOFF");
        orderService.modify(order);
    }

    private void stockOut(@NonNull Order order) {
        List> list = convertOrderItemsToList(order);
        inventoryService.stockOutForList(list);
        log.debug("stock out for orders");
    }

    private List> convertOrderItemsToList(Order order) {
        List> list = new ArrayList<>();
        for(OrderItem orderItem : order.getOrderItems()) {
            Map map = new HashMap<>();
            map.put("id", orderItem.getProductId());
            map.put("quantity", orderItem.getQuantity());
            list.add(map);
        }
        return list;
    }
...
}

在这里可以看到,OrderAggService通过注入依赖引入了OrderService, AccountService, InventoryService。OrderService在本地,而另外两个需要在微服务之间进行远程调用,最后存储在另外两个数据库中。因此,在payoff()方法中,通过注解@GlobalTransactional来实现分布式事务,这也使得OrderAggService增加了对分布式事务Seata框架的依赖。然而,OrderService, AccountService, InventoryService都是纯洁的,只有OrderAggService有对其它技术框架的依赖,从而从一定程度上缓解了业务代码与技术框架的耦合。案例详解我的仓库

DDD落地实现的深水区(5)整洁架构落地(下)_第3张图片

除了分布式事务,以上案例也可以通过领域事件的通知来实现。当用户下单时,OrderService仅仅实现保存订单,然后发起一个“下单”事件。当其它的微服务收到这个事件以后,再去完成后续其它的业务就可以了。这时,如何实现一个“领域事件的发起”呢?它需要一个分布式消息队列,而这个消息队列到底使用RabbitMQ, RocketMQ还是Kafka呢?还是那句话,高手从来不做选择,全都要。

首先,在支持DDD的技术平台中,要设计一套事件通知机制,而这套机制使用SpringCloud Stream来完成消息的发送与接收。SpringCloud Stream是微服务中消息通知的通用框架,它有RabbitMQ, RocketMQ, Kafka等不同的实现jar包,通过POM.xml引入,就可以对不同的消息队列发送消息。然而,采用SpringCloud Stream仅仅是目前技术的一种选择,今后同样可能会选用其它框架予以替换。所以,在平台中,它同样是在接口层中被封装成了DomainEventPublisher, DomainEventReceiver等通用接口。有了这些接口,上层的Service就可以通过调用它们来实现领域事件的发布与接收。接着,在bootstrap.yml的配置文件中,如果选用RabbitMQ有RabbitMQ的配置,选用RocketMQ有RocketMQ的配置,选用Kafka有Kafka的配置。这样,在软件开发阶段并不做出技术选型的决策,而是在系统上线以后,根据客户的不同需求进行配置,来满足客户的需求。这就是顶级架构师的设计思想。

有了领域事件发布的机制,当用户下单时,通过OrderAggService接收到这个请求,然后在placeOrder()方法中,先调用OrderService创建订单,然后发布“下单”事件,将整个订单对象作为消息进行发送:

@Service("orderAgg")
@Slf4j
public class OrderAggServiceImpl implements OrderAggService {
    @Autowired
    private OrderService orderService;
    @Autowired @Qualifier("placeOrderEvent")
    private DomainEventPublisher placeOrderEvent;
    @Autowired @Qualifier("returnGoodsEvent")
    private DomainEventPublisher returnGoodsEvent;
    @Override
    @Transactional
    public Long placeOrder(@NonNull Order order) {
        Long orderId = orderService.create(order);
        log.debug(String.format("create an order: [orderId: %d]", orderId));
        placeOrderEvent.publish("OrderAggService", order);
        return orderId;
    }
    @Override
    @Transactional
    public void returnGoods(@NonNull Long orderId) {
        Order order = orderService.load(orderId);
        orderService.delete(orderId);
        log.debug(String.format("return the goods: [orderId: %d]", orderId));
        returnGoodsEvent.publish("OrderAggService", order);
    }
}

完成了消息的发布,OrderAggService的任务就完成了。然而,它只完成了订单的创建,后续还有一系列操作需要完成。但后续还有哪些操作呢?通过消息队列的订阅,谁订阅了就能收到消息。客户微服务、库存微服务可以订阅这个消息,但它们各自在收到消息以后,会执行各自不同的操作。客户微服务收到消息以后,调用PaymentService完成支付;库存微服务收到消息以后,调用InventoryService完成库存扣减。因此,每个要订阅消息的微服务,在DomainEventReceiver的apply()方法中,各自去定义各自的操作:

@Component
@EnableBinding(PlaceOrderEventClient.class)
@Slf4j
public class PlaceOrderEventReceiver implements DomainEventReceiver {
    @Autowired
    private AccountService paymentService;
    @Override
    @StreamListener("placeOrder")
    public void apply(DomainEventObject event) {
        Order order = event.convertToEntity(Order.class);
        if(order==null||order.getPayment()==null)
            throw new ValidException("Didn't get the order and its payment");
        Payment payment = order.getPayment();
        Double balance = paymentService.payoff(payment.getAccountId(), payment.getAmount());
        log.debug(String.format("pay off for the order: [orderId:%d,accountId:%d,amount:%f,balance:%f]",
                order.getId(),payment.getAccountId(),payment.getAmount(),balance));
    }
}

通过以上的设计,每个Service只关心各自的业务,而与技术实现无关。当它们需要发布领域事件时,就调用相应的领域事件对象完成发布。领域事件的发布与接收实际上也是业务的一部分。至于如何通过技术,将消息发送到对方,则是技术的事情,被封装在了底层的技术框架中,从而满足了整洁架构的设计思想。案例详解我的仓库

如果对以上内容感觉有用,欢迎关注、点赞、转发!

通过这几期的内容,我从技术上给大家讲解了支持DDD的整洁架构设计。下一期,我将换一个角度,从管理的角度讲解,如果组织决定向DDD转型,该如何去规划与组织,如何制定DDD转型路线图。

(待续)

你可能感兴趣的:(领域驱动设计,整洁架构,领域驱动设计,DDD,架构设计,微服务)