深入实践DDD:以DSL驱动复杂软件开发
上QQ阅读APP看书,第一时间看更新

1.4.3 防腐层

我们创建的应用总是免不了要与外部的应用发生交互。与外部应用交互时,应该考虑使用六边形架构[1]风格。

在传统的分层架构风格中,上层的软件构造块(元素)依赖于下层的构造块。六边形架构风格摒弃了这种观点。它认为,一个软件构造块如果需要与外部软件元素进行交互来实现功能、完成自己的使命,那么它也只应该依赖于抽象。这个抽象就是它对外部软件元素的期望、设想。这其实也是依赖倒置原则[2]的应用。

扩展一下上面的在线外卖应用的例子。假设为了实现这个应用,我们还需要两个服务参与其中:

·Consumer Service(消费者服务),它的职责是处理对消费者信息的查询和验证逻辑。

·Delivery Service(送餐服务),这个服务负责送餐业务流程的管理。

这两个服务真正的业务逻辑可能并不需要外卖应用的开发团队去实现。也许送餐本来就是由第三方公司提供的服务,对方已经有了管理送餐业务的软件(下面假设这个软件叫送餐系统);也许有些业务是公司内其他部门的团队负责的,并且已经有了“现成的”系统,比如Consumer Service需要的信息在公司内的CRM系统中本来就存在。不管怎么说,现在“送餐服务”和“消费者服务”的业务逻辑都不需要外卖应用的开发团队去实现了,大家要做的只是把已有的应用与外卖应用进行集成。

总之,不管是外部的送餐系统还是公司已经存在的CRM系统,都不在外卖应用开发团队控制的范围之内,它们是另外两个上下文。但是外卖应用仍然可以提出它希望这些外部服务“看起来是什么样子的”,比如:

·它希望Consumer Service有一个verifyConsumer方法。调用这个方法可以验证某些消费者的信息是否有效。

·它希望Delivery Service有一个scheduleDelivery方法。若使用订单信息作为参数调用这个方法,就可以请求送餐服务的提供者对订单进行派送。

·它希望送餐服务的提供者“知道”它提供了一个叫作NoteOrderDelivered的通知接口。在送餐员把食物送到消费者手中之后,它希望这个接口被调用,且它能收到一个类型为OrderDelivered(订单已完成派送)的事件,然后根据事件信息做进一步的处理。

外卖应用对外部系统的这些期望都是“单相思”。它才不管外部的CRM系统有没有一个名字叫作verifyConsumer的方法呢,也许在CRM里面根本就没有Consumer这个概念,只有Customer的概念。它也不管在送餐员完成派送后送餐系统能不能发布一个类型为OrderDelivered的事件,也许在送餐系统里面根本没有Order的概念,只有Delivery的概念,类似的事件叫作DeliveryCompleted。

外卖应用不管这些,它就要说“Consumer”“OrderDelivered”,而不管外部系统的叫法。它把“想要的东西”都按照自己的想法定义了接口,也就是说,现在Consumer Service和Delivery Service都是外卖应用定义的接口——这就是所谓的“依赖于抽象,不依赖于实现”。外卖应用的这些抽象,都属于限界上下文的一部分。在自己的上下文内,只要能保证概念完整性即可,外卖应用没有什么理由改变自己,保持纯粹就挺好的。

如果外卖应用未能抵制住诱惑,让外部系统的概念渗透进了内部,比如在代码里面一会儿称客人为Consumer,一会儿又叫人家Customer——如此不正经,那就是“腐化”堕落。

如果外部的CRM系统和送餐系统也不想改变自己,那么矛盾如何解决?怎样把三观不同的系统“整”到一起?这就需要防腐层(Anti-Corruption Layer)在中间牵线搭桥、互相撮合了。

对于采用六边形架构的应用来说,防腐层一般是作为适配器来实现的(如图1-1所示)。

六边形架构的适配器是限界上下文的核心业务逻辑组件与外部系统之间的交互接口(又称为端口)的实现。适配器分两类:出站适配器与入站适配器。一个是适配器是“出站”还是“入站”是站在核心业务逻辑组件的角度看的。如果交互是请求/响应模式的调用,出站适配器实现对外部系统的调用,入站适配器处理外部系统的请求、调用内部的业务逻辑。如果交互是异步消息通信,那么出站适配器实现对外发送消息,而入站适配器接收外部发送过来的消息。

图1-1 六边形架构与防腐层

假设,外卖应用上下文在使用CRM或送餐系统提供的服务时属于“弱势客户”,那么外卖应用的开发团队就需要自己构造防腐层,防止“强势的供应商”(CRM上下文或送餐系统上下文)一方的概念侵入自己的上下文中。

在图1-1中,外卖应用的开发人员最终实现了三个适配器:

·为Consumer Service接口实现了一个出站适配器CRM Consumer Service Adaptor。

·为Delivery Service接口实现了一个出站适配器3rd Party Delivery Service Adaptor。

·为NoteOrderDelivered通知接口实现一个入站适配器3rd Party Delivery Service Notifier。

防腐层的代码就位于这三个适配器的内部。

我们在图1-1中用虚线划出了三个限界上下文的边界。只有那些包含防腐层代码的适配器是横跨不同的限界上下文的边界的,它们负责在不同上下文之间对概念进行翻译和转换。虽然外卖应用最终(在运行时)还是需要使用这些适配器才能实现完整的功能,但是就代码的依赖关系而言,是适配器依赖外卖应用定义的那些接口——这些接口又叫抽象,它们属于外卖应用核心业务逻辑组件的一部分。开发人员都知道这一点:当一个类实现一个接口时,依赖关系是类依赖接口,而不是接口依赖类。

总之,限界上下文是需要时刻保护好的概念的边界。需要将某个上下文的概念转换为另一个上下文概念的地方就是防腐层。最好让防腐层的代码和其他软件元素(构造块)之间存在明显的物理边界,倒不是说一定要把防腐层部署为一个个独立的服务或在独立的进程中运行,但是,最少我们还可以考虑将一个防腐层作为独立的类库项目进行构建和维护。

[1] Hexagonal architecture (software), https://en.wikipedia.org/wiki/Hexagonalarchitecture(software)。

[2] Dependency Inversion Principle, https://en.wikipedia.org/wiki/Dependencyinversionprinciple。