什么是DDD

140 阅读14分钟

DDD(Domain-Driven Design:领域驱动设计)不是一种架构,而是一种架构方法论,是一种拆解业务、划分业务、确定业务边界的方法,是一种领域设计思想。

  • 核心思想:建立领域模型,领域模型处于架构的核心位置。
  • 核心目标:避免业务逻辑的复杂度与技术实现的复杂度混淆在一起。

DDD包括战术设计和战略设计两部分。

  • 战略设计:侧重于高层次、宏观上去划分和集成限界上下文。
  • 战术设计:关注更具体使用建模工具来细化上下文。

Strategic DDD(战略设计)

  1. Bounded Context(限界上下文):用来界定领域边界。
  2. Context Mapping(上下文映射图、上下文图):用来描述系统关系。主要有以下几种关系:
    • Shared Kernel(共享内核)
    • Customer/Supplier(客户/供应商)
    • Conformist(追随者)
    • Anticorruption Layer(防腐层)
    • Open Host Service(公开主机服务)
    • Published Language(发布语言):通常与Open Host Service一起使用,用于定义开放主机的协议。
  3. Ubiquitous Language(通用语言) 团队统一的语言,是能够简单、清晰、准确的描述业务规则和业务含义的语言。

1.1 Bounded Context(界限上下文,BC)

在实施DDD的时候,我们要保证每一个术语应该仅表示一种领域概念,即:将用到的每一个术语进行限界划分。

image.png

Bounded Context定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一含义。

Bounded Context定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,不应该在模型中实现。

注意:处于不同界限上下文中,领域模型一定不可以共用。

1.1.1 领域拆分

随着业务的继续发展,所有领域、子域都有可能面临再次拆分的可能。 一个领域可以是一个独立的微服务,而实际上微服务正是领域拆分的结果。 如果不考虑技术异构、团队沟通等因素,一个限界上下文理论上就可以设计为一个微服务。

领域拆分的过程其实就是划分Bounded Context的过程,每一个Bounded Context就是一个领域。

1.1.2 领域

领域根据核心程度不同,分为Core Domain、Supporting Domain、Generic Domain。

Core Domain(核心领域): 公司的业务核心。例如电商业务中,商品、购物车、交易、促销、优惠、支付等都属于核心领域

Generic Domain(通用领域): 通用的领域,没有个性化的需求,甚至是各个公司都类似的功能或市场上可以直接购买到,可以被多个子域使用的领域,例如:用户、权限、认证、人脸识别等。

Supporting Domain(支撑领域): 一般是只不是系统中的最核心模块,但是也不是通用的组件和服务,但是对核心业务起到了支撑的作用的模块。

1.1.3 Ubiquitous Language(通用语言)

通用语言是:团队统一的语言,是能够简单、清晰、准确的描述业务规则和业务含义的语言。 通用语言的价值:

1. 解决各岗位的沟通障碍问题,确保业务需求的正确表达。

  • 如果没有通用语言,因为业务、产品、开发、测试的角色和术语不同,经常会遇到battle了很久,结果说的是同一个事情。
  • 如果没有通用语言,产品、开发、测试经常不能达成一致,导致开发的内容和业务诉求不同。

2. 通用语言贯穿于整个设计过程,能准确的把业务需求转化为代码。

  • 通用语言中的名词一般可以给领域对象命名。例如:订单、商品可以对应到领域中的一个实体。
  • 通用语言中的动词一般对应一个动作或领域事件。例如:订单已支付,订单已发货都对应一个领域事件。

1.2 Context Map(上下文映射图)

Context Map描述的是各个系统之间关系的总体视图,有以下几种关系

1.2.1 Shared Kernel(共享内核)

在某些情况下,两个团队间有一部分共同的功能,那么针对这部分,就称之为共享内核。

因为对于这部分是共同影响了两个团队,所以,对于共享内核的边界性就会要求很高。也就是说,每当团队彼此要跨入共享内核边界内的话,都是需要两个团队共同协商的,而不能仅仅由某一个团队只针对于自己功能进行对共享内核的修改。这种情况会比较特殊,一般来说,这种偏共享的部分会在后续的系统演变中被抽象化为平台服务,即:一种类型的支撑子域,并且由某一个指定的研发团队专门对这部分服务进行推进和维护。

对于共享内核来说,其产生的最主要原因还是在于对研发成本的节约对研发效率的提升,并且可以有效的防止多个团队之间去重复的“造轮子”。通过对相同业务或功能的代码维护,使其越来越平台化。

1.2.2 Customer/Supplier(客户/供应商)

分布式开发中随处可见,即接口提供者就是Supplier(或Provider),接口消费者就是Customer(或Consumer)。

1.2.3 Conformist(追随者)

有些场景中,一个系统(例如:A)的状态变化会直接影响另一个系统(例如:B)的结果,并且当A系统状态变化了以后B系统状态必须变化,这种系统关系即为Conformist。 例如:支付系统已经完成了支付,支付订单的状态已经变成「已支付」,那么交易系统的订单状态也必须变化,变成「买家已付款」。

1.2.4 Anticorruption Layer(防腐层)

如果依赖的系统设计的不友好,不适合当前系统的场景,降低系统间依赖和耦合,就需要使用防腐层(Anticorruption Layer)模式。

1.2.5 Open Host Service(公开主机服务)

就是将系统的一组服务暴露出去,给其他系统使用。例如微服务开发中的给其他系统使用接口(Service)。

1.3 Tactical DDD(战术设计)

1.3.1 Aggregate(聚合) & Aggregate Root(聚合根)

定义: 在领域模型中,我们将紧密联系的个体聚合在一起,按照组织内统一的业务规则完成特定的业务功能,这就是聚合。

例如,在电商中,主订单、订单明细他们的业务规则相同,而且基本上都是一同操作的,对订单进行操作的时候,基本上都会同时修改主订单和订单明细,那么主订单和订单明细就是一个聚合。在这个聚合中,操作的入口基本都是主订单,所以主订单就是这个聚合的聚合根。


注意: 聚合内的内容具有一致性,即:需要在事务中修改一个聚合的内容。如果没有一致性要求,那么应该就不属于一个聚合。

通过唯一标识来引用其他聚合或实体。

如果聚合创建复杂,推荐使用工厂方法来屏蔽内部复杂的创建逻辑。

在传统数据模型中,一般认为每个实体都是对等的,可以单独修改任意一个实体;在DDD中,聚合内对象的修改必须按照统一的业务规则来完成,聚合是数据修改、持久化的基本单元。

示例: 交易系统中的订单包括主单、明细,他们就是一个聚合,主单就是这个聚合的聚合根。 商品系统中的商品包括Item(商品)、SKU(商品的库存单元),他们也是一个聚合,其中Item就是一个聚合根。


聚合设计的原则: 设计小聚合。小聚合可以降低数据冲突,规避业务过大。 通过唯一标识引用其他聚合。 聚合内保持数据强一致,聚合外保持数据最终一致。 通过应用层实现跨聚合调用。

1.3.2 Entity(实体)

定义: 有一对象拥有唯一标识(一般是id),在经历各种状态变化后,唯一标识依然保持不变,对这种对象而言,重要的是具有延续性的唯一标识,而不是属性。领域中这种对象称为实体。 实体一般对应业务对象,拥有属性和业务行为。 实体是基础的领域对象(Domain Object)。


示例: DB表中的数据加载到内存中以后就变成一个实体,我们都是通过db主键来区别不同的记录。

1.3.3 Value Objects(值对象)

定义: 无唯一标识的简单对象。 其唯一标志不重要,重要的是其属性,其描述的是领域中的一个信息,这种对象称为值对象。 值对象是属性集合,是对实体信息的描述。 值对象也是基础的领域对象(Domain Object)


示例: 订单对象中的商品信息、地址信息就是值对象。

1.3.4 Domain Services(领域服务)

一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。

1.3.5 Domain Events(领域事件)

领域事件是对领域内发生的活动进行的建模。

1.3.6 Factory(工厂模式)

在创建对象时,有些聚合需要实体或值对象较多,或者关系比较复杂,为了确保聚合内所有对象都能同时被创建,同时避免在聚合根中加入与其本身领域无关的内容,一般会将这些内容交给Factory处理。

Factory的主要作用:封装聚合内复杂对象的创建过程,完成聚合根、实体、值对象的创建。

1.3.7 Repository Model(仓储模式)

为了避免基础层数据处理逻辑渗透到领域层的业务代码中,导致领域层和基础层形成紧密耦合关系,引入Repository层。 Repository分为Interface和Implement,领域层依赖Repository接口。

1.3.8 Modules(模块)

在创建系统的时候,我们一般会根据负责的内容,将一个系统划分为多个模块,每个模块一般和子领域对应。

1.4 从领域划分到系统落地

以电商平台为例,DDD战略设计指导微服务落地如下图所示。

将电商领域进行细分,然后将业务相近、耦合紧密的领域聚合在一起,落地成我们的业务系统。

每个领域都有很多内容,以商品领域为例,将商品领域进行进一步细分,可以分为类目、属性、属性值、SPU、Item、SKU,然后关系紧密的内容据合在一起,形成一个个的聚合。

1.5 领域模型

1.5.1 贫血模型(Anemic Model)

贫血模型是值领域对象中:只数据没有行为。即:模型中只有属性、set、get方法,逻辑放在业务逻辑层(Service/Manager)中。 只有数据没有行为的对象不是真正的对象,所以贫血模型是一种反模式,和面向对象设计相违背。领域对象只是作为保存状态或者传递状态使用,在业务逻辑层处理所有的业务逻辑,对于细粒度的逻辑处理,通过增加一层Facade达到门面包装的效果。

一般在使用Spring的项目中,这种贫血模型随处可见,这种系统结构层次简单清晰,即:Consumer/Api -> Service -> Manager/Biz -> Dao -> Mybatis -> DB。

贫血的领域对象起的作用是:只传递数据,不包含任何业务逻辑。领域对象如果不包含逻辑,将会在持续的迭代升级中,给开发、维护工作带来大量成本。

1.5.2 充血模型

面向对象设计的本质是:“一个对象是拥有状态和行为的”,充血模型就是那种即拥有属性、又拥有操作的类。 修改一个用户信息,然后保存,在贫血模型的场景中示例代码如下:

user.setXXX();
userManager.save(user);

在充血模型的场景中,代码如下所示:

user.setXXX()
user.save();  保存自己

优点:是面向对象的;Service符合单一职责。

缺点:那些逻辑放在Domain Object中,那些逻辑放在Service中,比较含糊。编码成本也比较高,事务控制的成本也会增加。

1.6 CQRS模式

1.6.1 CQRS简介

CQRS(Command Query Responsibility Segregation)是将Command(命令)与Query(查询)分离的一种模式。 其基本思想在于:任何一个方法都可以拆分为命令和查询两部分:

Command:不返回任何结果(void),但会改变对象的状态。 Command是引起数据变化操作的总称,一般会执行某个动作,如:新增,更新,删除等操作。 操作都封装在Command中,用户提交Commond到CommandBus,然后分发到对应的CommandHandler中执行。Command执行后通过Repository将数据持久化。 事件源(Event source)CQRS,Command将特定的Event发送到EventBus,然后由特定的EventHandler处理。 Query:返回查询结果,不会对数据产生变化的操作,只是按照某些条件查找数据。 基于Query条件,返回查询结果;为不同的场景定制不同的Facade。

1.6.2 CQRS三种模式

1.6.2.1 单数据库的CQRS

顾名思义,双方都在和一个数据库对话。Command 在域中执行用例,从而修改实体的状态,然后通过 ORM 如 Entity Framework Core 或 Hibernate 将实体保存到数据库中。

Query 直接通过数据访问层执行,数据访问层要么是使用各种 ORM,要么通过存储过程。 image.png

1.6.2.2 读写分离的CQRS

image.png CQRS不只是为了分离数据的写入和读取,它的根本目的是为了实现数据的多重表示,每一种表示都能够满足某些用户的需求。 CQRS可能会有多种查询模式,可以使用数据库、Redis,ES等等。例如对于复杂的数据查询诉求,Command负责将数据落到DB中,然后同步到ES中,Query端从ES中查询需要的数据。

1.6.2.3 事件源的CQRS

image.png 当Command系统完成数据更新的操作后,会通过「领域事件」的方式通知Query系统。Query系统在接受到事件之后更新自己的数据源。所有的查询操作都通过Query系统暴露的接口完成。

1.6.3 CQRS架构的优点

  • Command、Query两端架构分离、相互不受束缚,各自独立设计、扩展
  • Command端通常结合DDD,解决复杂的业务逻辑;
  • Query端轻量级查询,多种不同的查询视图通过订阅事件来更新
  • Command端通过分布式消息队列水平扩展,天然支持削峰
  • EDA架构(Event-Driven Architecture, 事件驱动架构),整个系统各个部分松耦合,可扩展性好
  • 架构层面做到无并发,实现Command的高吞吐
  • 技术架构和业务代码完全分离,程序员不用关心技术问题,更方便的分工合作

1.6.4 CQRS架构的缺点

  • 需要处理事务问题,开发成本提高。例如一个Command可能需要修改多个DB,数据一致性处理成本较高。CQRS不是强一致性,而是面向最终一致性
  • 实效性问题。Command端修改后同步给Query端可能存在时间差,那么Command修改数据后、Query可能查询到旧数据。
  • Event传递需要稳定且性能强大的分布式消息队列
  • 必须有强大可靠的CQRS框架,从头做起成本高、风险大
  • 最好结合Event Sourcing模式,否则Command、Query分离意义不大
  • 提高了开发人员的门槛