不容置疑的鲜有真理,多为裹着权威的谬误。

01. 引言

作为一名患有重度代码洁癖的程序员,最让我苦恼的就是在工作中去修改别人写的代码了。每每看到大量晦涩、冗余、没有细细思考就胡乱写就的代码,我都先得一行一行地去理解这个人想要通过这一长串代码(通常是放在一个无所不能的Service里面的)去做什么事的时候,我就越来越觉得所有程序员都应该去学学设计模式,至少面向对象语言的程序员们是非常有必要的。

但另一方面,设计模式在很多情况下都作为一种可有可无的软性技能存在,并且很多时候由于项目赶工的因素,所以大部分软件开发团队也不会强制每位成员必须100%去遵循特定的设计模式。且各种不同来源的设计模式建议常常相互冲突。没有一种设计模式是非遵循不可的,这就是尴尬之处。

可是话又说回来,遵循常见的设计模式尽管在一开始可能显得有点繁琐,可是我觉得是利大于弊的,是能显著提高代码质量,减少程序的Bug数量的。

作为领域驱动设计(Domain Driven Design, DDD)的忠实粉丝,我想在这篇文章中讲讲领域驱动设计的优点。

我和DDD的初次邂逅还是在第一家公司工作的时候。那个时候对设计模式甚至都知之甚少,可是有一天我被项目组长分配去修改一个此前从没接触的微服务模块的功能。克隆好这个模块的代码试着看了一下,看到各种不知所云的application、domain、infrastructure的包名,骨子里都刻着传统Web三层架构的基因的我顿时感到手足无措了。于是不得不去了解一下这是种什么设计模式。最终找到了Eric Evans的《领域驱动设计》这本书。可是Eric Evans在这本书里主要从思想层面抽象地阐述了这种设计模式,所以理解曲线对我来说有些过于陡峭了。然后我找来了Vaughn Vernon的《实现领域驱动设计》这本书,这本书更多的是从具体实现去解释DDD,读完觉得比Eric Evans的书更通俗易懂些。最后读了国内作者张译的《解构领域驱动设计》,又加深了对于领域驱动设计的理解。(顺便提一下,《解构领域驱动设计》的作者张译就是我第一家公司的技术总监,也是《实现领域驱动设计》的中文译者。现在想来也好荣幸能和这样优秀的前辈共事过)。

02. 什么是领域驱动设计

领域驱动设计是对面向对象设计的一种补充,一开始是Eric Evans在《领域驱动设计》一书中展示的一整套设计模式语言。

领域驱动设计,顾名思义,即把关注点放在领域建模的程序设计,即以领域专家的视角建立的与该领域匹配的软件模型。技术实现则是完成系统业务目标的辅助手段。明确地划分了领域与技术实现之间的边界,隔离技术实现对领域建模的干扰。

03. 领域、子域和限界上下文

理解领域、子域和限界上下文的三个概念对于理解什么是领域驱动设计至关重要。

一般来说,领域(Domain)是软件开发过程涉及到的现实世界的业务概念,是需求分析师写的需求文档内的业务需求,也是软件系统中程序需要实现的功能。

“领域”的概念既可以广义地泛指一个组织所涉及的所有业务,也可以狭义地特指“领域”之下的其中一个“子域”。继续以上面的图书电商系统为例,整个图书电商系统可以是一个“领域”,图书电商系统下的产品业务、订单业务、物流业务也可以是三个不同的“领域”,产品业务、订单业务和物流业务同时也是隶属于图书电商“领域”的“子域”。

限界上下文则是由明确的边界圈定的语境,这种边界既可以是多个系统组成的系统集群的系统边界,也可以是多模块项目中的模块间的边界,还可以是语言的包目录划分的界限。理想情况下,一个限界上下文只容纳一个子域,但也可能容纳多个子域。

一个良好实施领域驱动设计的项目应该体现“高内聚、低耦合”的设计原则,高度内聚的术语应该放到一个上下文内,并尽可能减少上下文之间的依赖,各领域在自治的边界内完成属于该领域的工作。以图书电商系统的“用户”为例,在登录系统、获取系统访问权限时,用户是被放在“权限认证”的上下文语境下讨论的;在获取图书的购买者信息时,用户则是被放在“产品”的上下文语境下讨论的。尽管“权限认证”上下文的“系统用户”与“产品”上下文的“读者用户”很可能代表一个人,但是它在不同的上下文中代表的角色和身份是不一样的。在这种情况下,我们就应该在不同的上下文中针对不同语境创建不同的领域模型。

04. 领域驱动设计的架构

领域驱动设计并不强制使用特定架构,因此,我们可以根据需要选择不同风格的架构。下面列举一些在DDD社区广泛应用的架构。

4.1 分层架构下的DDD

分层架构下的DDD模式由经典的Web三层架构(包含用户界面层、业务逻辑层和数据访问层)改良而来。分层架构的DDD在用户界面层和业务逻辑层之间引入了应用层,同时将业务逻辑层更名为领域层,数据访问层更名为基础设施层,表明该层次不仅用于封装数据访问的技术实现的语义。

DDD之父Eric Evans对各层做了简单的描述:

层次 职责
用户界面层(展现层)
负责向用户展现信息以及解释用户命令
应用层
定义软件要完成的任务,并指挥表达领域概念的对象来解决问题。不包含业务逻辑,不保留业务状态,但保留应用任务的进度状态
领域层
业务软件的核心。用于表达业务概念、业务状态信息以及业务规则。
基础设施层
为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件等

分层架构遵循“技术为本,用户至上”的认知规则,层次越往上,就越面向用户和业务;层次越往下,就越通用,越面向技术。层间关系是正交的。至于引入应用层,则是为了给调用者提供完整的业务用例,使调用者无需与细粒度的领域模型直接协作。

自顶向下的分层结构并不意味着我们必须使用自顶向下的依赖模式。根据面向对象设计的依赖倒置原则(Dependency Inversion,DI),高层模块不应该依赖于低层模块,二者都应该依赖于抽象。遵循这一原则,作为调用者的高层模块应该依赖低层模块的抽象。这样的依赖方式可以避免高层模块受制于低层模块。

以一个使用分层架构DDD的书店系统的订单上下文为例,我们可以将不与具体技术实现挂钩的领域事件发布者接口放到领域层,将基于 RabbitMQ 的领域发布者实现放到基础设施层(也即防腐层)。

com.bookstore.order.domain 包下的 Event Publisher 接口:

				
					package com.bookstore.order.domain;

public interface EventPublisher {
    void publish(Event... events);
}
				
			

com.bookstore.order.infrastructure.messaging包下的 Event Publisher 实现:

				
					package com.bookstore.order.infrastructure.messaging;

public class RabbitEventPublisher implements EventPublisher {

    @Override
    public void publish(Event... events) {

        Stream.of(events).parallel().forEach(event -> {
            if (event instanceof OrderCreated) {
                queueOrderCreated.offer((OrderCreated) event);
            }
            //...
        });
    }
}
				
			

依赖于EventPublisher的Order应用服务调用EventPublisher发布订单生成的领域事件:

				
					package com.bookstore.order.application;

public class OrderApplicationService {
	private EventPublisher eventPublihser;

    public void createOrder(OrderingRequest request) {
        Order order = Order.create().ofBook(request.getBookId()).ofAmount(request.getAmount());
        BookReview bookReview = bookClient.check(order);
        if (bookReview.isUnavailable()) {
            throw InvalidOrderException.unavailableBook();
        }
        orderRepo.save(order);
        OrderCreated orderCreated = new OrderCreated(order.id(), order.bookId(), order.amount(),
                order.createdAt());
        eventPublisher.publish(orderCreated);
    }
}
				
			

这样,如果后续想要更换消息中间件实现,只需要替换掉基础设施层的 RabbitEventPublisher,领域层的 EventPublisher 接口及依赖该组件的下游调用者不受影响。层次之间的变化互不干扰。

4.2 六边形架构下的DDD

相比于分层架构从上到下的层级结构,六边形架构呈现出从外到内的层级结构,将领域逻辑封装在六边形的边界内,通过入口和出口两个方向的端口和适配器与外界通信。就像洋葱一样,最有价值的部分层层包裹在洋葱皮内。

flowchart LR A(客户端) --> B(入口适配器) subgraph 外部边界 B --> C(入口端口) subgraph 内部边界 C --> D(应用程序) D --> E(出口端口) end E --> F(出口适配器) end F --> G[(基础服务)] style A fill:#FFFFFF; style B fill:#808080; style C fill:#808080; style D fill:#fffb00; style E fill:#808080; style F fill:#808080; style G fill:#FFFFFF;

六边形架构中的端口是解耦的关键。入口端口体现了“封装”的思想;出口端口体现了“抽象”的思想。

一个六边形架构的程序看起来可能像下面这样:

05. DDD的核心要素

5.1 实体

实体是具有唯一标识的领域对象,其核心特征是它的身份标识在整个生命周期中保持不变。即使实体的其他属性发生变化,只要标识不变,它仍然是同一个实体。

例如,在电商系统中,订单就是一个典型的实体。订单可能有各种状态变化(已创建、已支付、已发货等),但只要订单ID不变,我们就认为它是同一个订单。

一个典型的实体应该具备3个要素:

  • 身份标识
  • 属性
  • 领域行为

身份标识(ID)是实体对象的必要标志,在DDD中,没有ID的领域对象就不是实体。由于实体的状态可以变更,这意味着我们不能根据实体的属性值判断其身份,如果没有唯一的ID,就无法跟踪实体的状态变更。实体的ID既可以是无意义的通用类型的值,也可以是有意义的蕴含领域概念的值,如订单的实体ID可能会包含下单渠道号、支付渠道好、业务类型、下单日期等信息。

实体的属性用以说明实体的瞬时特征。属性既可能是不可再分的原子属性,也可能是通过自定义类型来表现的组合属性。

如将Product实体的属性定义为:

				
					public class Product {
    private String id;
    private int quantity;
    private Category category;
    private Weight weight;
    private Price price;
}
				
			

很多面向对象语言的程序员喜欢给实体堆砌一大堆属性而忽略给实体定义操作这些属性的方法,每当需要变更实体的状态就在业务服务中通过实体getter和setter方法去操作实体状态。这种做法实在有些得不偿失,我们称这样的对象为“贫血对象“,这样的实体只是作为属性容器存在,而不是程序中的有机组成部分。DDD提倡为实体定义表达领域行为的方法而不是让外部调用者来操作自己的状态,体现了“职责分治”的思想。

实体的领域行为用以操作对象状态的变换,修改的只是对象的内存状态,与持久化无关。

5.2 值对象

值对象是没有概念上标识的对象,它们通过属性值来定义。值对象通常是不可变的,两个值对象如果所有属性都相同,则认为它们是相同的。

比如,地址就是一个典型的值对象。如果两个地址的省、市、街道等信息完全相同,那么它们就是相同的地址,不需要额外的标识来区分。

5.3 聚合

聚合是一组相关对象的集合,它定义了领域对象的边界和所有权关系。每个聚合都有一个根实体(Aggregate Root),外部对象只能通过聚合根来访问聚合内的其他对象。

以电商系统为例,订单和订单项可以组成一个聚合,其中订单是聚合根。外部系统要修改订单项,必须通过订单这个聚合根来进行操作。

5.4 领域服务

当某些业务逻辑不适合放在实体或值对象中时,我们可以使用领域服务。领域服务封装了领域知识,通常处理跨多个聚合的业务逻辑。

例如,”转账”操作涉及两个账户的余额变化,这种跨实体的操作就适合放在领域服务中实现。

5.5 仓储

仓储是用于持久化和检索聚合的抽象。它隔离了领域模型和数据访问技术,使得我们可以独立地设计领域模型而不必考虑持久化细节。

仓储通常只保存和获取聚合根,因为聚合内部的对象的生命周期由聚合根管理。

5.6 领域事件

领域事件表示领域中发生的值得关注的事情,表达了实体的状态变更和迁移。通过发布和订阅领域事件,我们可以实现松耦合的领域对象之间的交互。

作为已经发生的事实,事件的命名应采用动词的过去形态,如订单已创建的事件可以被命名为“OrderCreated”。

				
					public class OrderCreated extends DomainEvent implements Event {
    private final Long orderId;
    private final Long bookId;
    private final int amount;
    private final LocalDateTime createdBy;
}
				
			

引用

  1. 《领域驱动设计——软件核心复杂性应对之道》(Eric Evans 著,人民邮电出版社)
  2. 《实现领域驱动设计》(Vaughn Vernon 著,电子工业出版社)
  3. 《解构领域驱动设计》(张译 著,人民邮电出版社)