Domain Driven Design(领域驱动设计, DDD),不是一种架构,而是一种架构方法论,是一种拆解业务、划分业务、确定业务边界的方法,是一种领域设计思想。
DDD包括战术设计和战略设计两部分。
Bounded Context(限界上下文):用来界定领域边界。
Context Mapping(上下文映射图、上下文图):用来描述系统关系。主要有以下几种关系:
Ubiquitous Language(通用语言)
团队统一的语言,是能够简单、清晰、准确的描述业务规则和业务含义的语言。
开始之前,先说思考一个问题:早上起床,妈妈让小明衣服能穿多少穿多少,那么小明是应该多穿衣服呢,还是少穿衣服呢?如果没有所处的环境,我们其实无法判断。因为如果是在夏天,妈妈的意思是让小明少穿衣服;如果是在冬季,妈妈意思是让小明多穿衣服。在程序的世界中,Bounded Context就是为了确定领域边界,让每一个事物的表达精确。
如果我们要设计一个电商平台,那么我们需要就要有商品、交易、支付、物流等模块,这些模块其实就是一个个的领域。在不同领域中,同样一个事物的因为关注点不同,其含义也不同,但是在确定的上线文中,其含义就是确定的。例如:
在不同场景中,我们对同一个事物的称呼也有较大差异。例如
Bounded Context定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一含义。
Bounded Context定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,不应该在模型中实现。
注意:处于不同界限上下文中,领域模型一定不可以共用。例如:上面示例中的商品/货物、商品/订单明细不可以使用同一个模型,不过这对于有经验的开发者而言也是显而易见的。
继续之前电商平台的示例,电商平台可以分为商品、交易、支付、物流几个领域,如图:
随着业务发展,这些领域可以进一步拆分出多个子领域出来,例如:商品可以拆分为:类目、库存、商品等领域;交易可以拆分为:交易、促销、优惠券、售后等领域;支付可以拆分为:支付核心、支付路由、支付渠道等领域。如图:
随着业务的继续发展,这里的所有领域、子域都有可能面临再次拆分的可能。熟悉微服务开发的小伙伴,看到这里应该就很清楚了,一个领域可以是一个独立的微服务,而实际上微服务正是领域拆分的结果。
如果不考虑技术异构、团队沟通等因素,一个限界上下文理论上就可以设计为一个微服务。
上面领域拆分的过程其实就是划分Bounded Context的过程,每一个Bounded Context就是一个领域。
领域根据核心程度不同,分为Core Domain、Supporting Domain、Generic Domain。
Core Domain(核心领域):
公司的业务核心。例如电商业务中,商品、购物车、交易、促销、优惠、支付等都属于核心领域
Generic Domain(通用领域):
通用的领域,没有个性化的需求,甚至是各个公司都类似的功能或市场上可以直接购买到,可以被多个子域使用的领域,例如:用户、权限、认证、人脸识别等。
Supporting Domain(支撑领域):
一般是只不是系统中的最核心模块,但是也不是通用的组件和服务,但是对核心业务起到了支撑的作用的模块。
通用语言是:团队统一的语言,是能够简单、清晰、准确的描述业务规则和业务含义的语言。
通用语言的价值:
Context Map描述的是各个系统之间关系的总体视图,有以下几种关系:
在电商场景中,购买不同品类的商品,其流程也会有非常大的差异。例如:买卖实物商品的流程一般是:买家付款、卖家发货、买家确认收货;买卖酒店房间的流程一般是:买家付款、卖家预留客房、买家入驻、买家退房结单;买卖旅游线路的流程一般是:买家付款、卖家确认、服务履约。
这些业务场景、交易流程虽然有较大的的差异,但是他们可以共同复用核心的交易流程,如下图所示:
可以不使用共享内核么?或者说共享内核对于系统架构来说有什么好处?
如上示例,从多个团队或业务中剥离的共享的子集,就是共享内核。
分布式开发中随处可见,即接口提供者就是Supplier(或Provider),接口消费者就是Customer(或Consumer)。
有些场景中,一个系统(例如:A)的状态变化会直接影响另一个系统(例如:B)的结果,并且当A系统状态变化了以后B系统状态必须变化,这种系统关系即为Conformist。
例如:支付系统已经完成了支付,支付订单的状态已经变成「已支付」,那么交易系统的订单状态也必须变化,变成「买家已付款」。
如果依赖的系统设计的不友好,不适合当前系统的场景,降低系统间依赖和耦合,就需要使用防腐层(Anticorruption Layer)模式。
就是将系统的一组服务暴露出去,给其他系统使用。例如微服务开发中的给其他系统使用接口(Service)。
定义:
在领域模型中,我们将紧密联系的个体聚合在一起,按照组织内统一的业务规则完成特定的业务功能,这就是聚合。例如,在电商中,主订单、订单明细他们的业务规则相同,而且基本上都是一同操作的,对订单进行操作的时候,基本上都会同时修改主订单和订单明细,那么主订单和订单明细就是一个聚合。在这个聚合中,操作的入口基本都是主订单,所以主订单就是这个聚合的聚合根。
注意:
聚合内的内容具有一致性,即:需要在事务中修改一个聚合的内容。如果没有一致性要求,那么应该就不属于一个聚合。
通过唯一标识来引用其他聚合或实体。
如果聚合创建复杂,推荐使用工厂方法来屏蔽内部复杂的创建逻辑。
在传统数据模型中,一般认为每个实体都是对等的,可以单独修改任意一个实体;在DDD中,聚合内对象的修改必须按照统一的业务规则来完成,聚合是数据修改、持久化的基本单元。
示例:
交易系统中的订单包括主单、明细,他们就是一个聚合,主单就是这个聚合的聚合根。
商品系统中的商品包括Item(商品)、SKU(商品的库存单元),他们也是一个聚合,其中Item就是一个聚合根。
聚合设计的原则:
设计小聚合。小聚合可以降低数据冲突,规避业务过大。
通过唯一标识引用其他聚合。
聚合内保持数据强一致,聚合外保持数据最终一致。
通过应用层实现跨聚合调用。
一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。
领域事件是对领域内发生的活动进行的建模。
在创建对象时,有些聚合需要实体或值对象较多,或者关系比较复杂,为了确保聚合内所有对象都能同时被创建,同时避免在聚合根中加入与其本身领域无关的内容,一般会将这些内容交给Factory处理。
Factory的主要作用:封装聚合内复杂对象的创建过程,完成聚合根、实体、值对象的创建。
为了避免基础层数据处理逻辑渗透到领域层的业务代码中,导致领域层和基础层形成紧密耦合关系,引入Repository层。
Repository分为Interface和Implement,领域层依赖Repository接口。
在创建系统的时候,我们一般会根据负责的内容,将一个系统划分为多个模块,每个模块一般和子领域对应。
将电商领域进行细分,然后将业务相近、耦合紧密的领域聚合在一起,落地成我们的业务系统。
每个领域都有很多内容,下面以商品领域为例,我们将商品领域进行进一步细分,可以分为类目、属性、属性值、SPU、Item、SKU,然后关系紧密的内容据合在一起,形成一个个的聚合。
贫血模型是值领域对象中:只数据没有行为。即:模型中只有属性、set、get方法,逻辑放在业务逻辑层(Service/Manager)中。
只有数据没有行为的对象不是真正的对象,所以贫血模型是一种反模式,和面向对象设计相违背。领域对象只是作为保存状态或者传递状态使用,在业务逻辑层处理所有的业务逻辑,对于细粒度的逻辑处理,通过增加一层Facade达到门面包装的效果。
一般在使用Spring的项目中,这种贫血模型随处可见,以下是使用贫血模型后,典型的系统结构图:
这种系统结构层次简单清晰,即:Consumer/Api -> Service -> Manager/Biz -> Dao -> Mybatis -> DB。
贫血的领域对象起的作用是:只传递数据,不包含任何业务逻辑。在[DDD-Domain Primitive](DDD-Domain Primitive.md)中有一些简单的小例子,介绍了领域对象如果不包含逻辑,将会在持续的迭代升级中,给开发、维护工作带来大量成本。
面向对象设计的本质是:“一个对象是拥有状态和行为的”,充血模型就是那种即拥有属性、又拥有操作的类。
修改一个用户信息,然后保存,在贫血模型的场景中示例代码如下:
user.setXXX();
userManager.save(user);
在充血模型的场景中,代码如下所示:
user.setXXX()
user.save(); 保存自己
典型的系统结构图:
优点:是面向对象的;Service符合单一职责。
缺点:那些逻辑放在Domain Object中,那些逻辑放在Service中,比较含糊。编码成本也比较高,事务控制的成本也会增加。
CQRS(Command Query Responsibility Segregation)是将Command(命令)与Query(查询)分离的一种模式。
其基本思想在于:任何一个方法都可以拆分为命令和查询两部分:
CQRS不只是为了分离数据的写入和读取,它的根本目的是为了实现数据的多重表示,每一种表示都能够满足某些用户的需求。
CQRS可能会有多种查询模式,可以使用数据库、Redis,ES等等。例如对于复杂的数据查询诉求,Command负责将数据落到DB中,然后同步到ES中,Query端从ES中查询需要的数据。
当Command系统完成数据更新的操作后,会通过「领域事件」的方式通知Query系统。Query系统在接受到事件之后更新自己的数据源。所有的查询操作都通过Query系统暴露的接口完成。
《中台架构与实践-基于DDD和微服务》
如何掌握DDD业务领域建模、数据库及聚合?
DDD 领域驱动设计:贫血模型、充血模型的深入解读
领域模型、贫血模型、充血模型概念总结
3种CQRS架构模式