对领域驱动设计的理解与实践

对领域驱动设计的理解与实践

更新于:2020-06-14

什么是DDD

领域驱动设计(Domain-Driven-Design)是一种针对大型复杂系统的领域建模与分析方法论。
2003 年,Eric Evans 发布《Domain-Driven Design: Tackling Complexity in the Heart of Software》(领域驱动设计:软件核心复杂性应对之道),其中定义了DDD。

DDD改变了传统软件开发针对数据库进行的建模方法;DDD先对业务领域进行分析,建立领域模型,根据领域模型驱动代码设计。合理运用面向对象的高内聚低耦合设计要素,降低整个系统的业务复杂性,并使得系统具有更好的扩展性,更好的应对多变的业务需求。

领域 Domain

一个领域就是一个问题域,只要是同一个领域,那问题域就相同。
只要确定了系统所属的领域,那么这个系统的核心业务,即要解决的问题以及问题的边界就基本确定了。

举例
陌生人社交领域,包含有聊天,用户推荐,朋友圈等核心环节。 只要是这个领域,一般都会有这些相同的核心业务,因为他们要解决问题的本质是一样的,就是交友。

每一个领域,都有一个对应的领域模型,领域模型能够很好的帮我们解决复杂的业务问题。

驱动设计

在DDD中,以领域(domain)为边界,分析领域的核心问题,再设计对应的领域模型,最后通过领域模型驱动代码设计的实现。这样设计的系统才有合理的分层与解耦,对以后业务的迭代开发,代码的维护才更加容易。

然而很多互联网公司,为了追求快速的上线,都是模型都没有想清楚就开始写代码,这就导致了后续代码维护困难,无法扩展。修改bug同时又引入新的bug,反反复复,进入恶性循环。
当然,这跟梳理清楚领域模型需要一定时间,这与初创型的互联网公司需求快速上线有点相悖,但是,这点时间的投入是非常值得的。因为可以避免了系统上线后不久又得重构的问题。

概念总结

  • 领域就是问题域
  • 模型驱动的思想:通过建立领域模型来解决领域中的核心问题
  • 领域建模的目标:针对我们在领域中核心问题,而不是整个领域中的所有问题
  • 领域模型设计:设计时应考虑一定的抽象性、通用性,以及复用价值
  • 代码实现:通过领域模型驱动代码的实现,确保代码让领域模型落地,代码最终能解决问题

为什么需要DDD

系统复杂性

耦合

随着产品不断的迭代,业务逻辑变得越来越复杂,系统也越来越庞大。模块彼此互相关联、耦合。导致增加或修改一个功能变得异常艰难,同时功能间的界限也变得模糊,职责不再清晰。这个时候就需要进行重构,拆分。

虽然架构本身是随着业务进行不断演进的;但是,如果架构初始设计不体现出业务的模型,那么新需求就无法体现在现有架构上,导致不断腐化,不断重构。

内聚

贫血模型 Anemic Domain Object

domain object仅用作数据载体,而没有行为和动作的领域对象。

指领域对象里只有get和set方法,没有相关领域对象的业务逻辑。业务逻辑放在业务层。

充血模型 Rich Domain Object

将业务逻辑和对象存储放在domain object里面,业务层只是简单进行小部分业务的封装及其他domain的编排。
面向对象设计,符合单一职责设计。

贫血 vs 充血

贫血模型的domain object很轻量,这导致业务层的复杂,domain object相关的业务逻辑散布在各个业务层,造成业务逻辑的冗余以及原本domain object的定义就变得相对模糊,这就是贫血症引起的失忆症。

而采用领域开发的方式,将数据和行为封装在一起,与业务对象相映射;领域对象职责清晰,将相关业务聚合到领域对象内部。

微服务

DDD 的本质是一种软件设计方法论,而微服务架构是具体的实现方式。微服务架构并没有定义对复杂系统进行分解的具体方法论,而 DDD 正好就是解决方案。

微服务架构强调从业务维度来分治系统的复杂度,而DDD也是同样的着重业务视角。

DDD能带来什么

  • 建立通用语言: 围绕领域模型建立的一种语言,团队所有成员都使用这种语言进行沟通和活动
  • 驱动代码设计:领域建立模型,模型指导设计,设计产出代码
  • 解决核心问题:模型的设计中心就是核心域,就是解决核心的问题

DDD建模

战略设计

战略设计就是从宏观角度对领域进行建模。划分出业务的边界,组织架构,系统架构。

DDD中,对系统的划分是基于领域的,也是基于业务的。

通用语言Ubiquitous Language

通用语言是指确定统一的领域术语,提高开发人员与领域专家之间的沟通效率。

一旦确定了统一语言,无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,清晰准确地定义领域知识。

当确认整个团队统一的语言后,就可以开始进行领域建模。

领域和子域

领域Domain
一个领域本质上可以理解为就是一个问题域。只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。

举例
- 社交领域:关键问题是用户推荐,聊天 - 电商领域:关键问题是购物,订单,物流

子域Subdomain

如果一个领域过于复杂,涉及到的领域概念、业务规则、交互流程太多,导致没办法直接针对这个大的领域进行领域建模。这时就需要将领域进行拆分,本质上就是把大问题拆分为小问题,把一个大的领域划分为了多个小的领域(子域),那最关键的就是要理清每个子域的边界

子域可以根据自身重要性和功能属性划分为三类子域: - 核心域:公司核心产品和业务的领域 - 支撑子域:不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域 - 通用子域:被多个子域使用的通用功能子域

每个领域的划分都不一样。对相同领域公司而言,其核心,支撑,通用的子域也可能有不一样的地方,但大体上基本都是一样的。

举例
社交领域的划分 - 核心域:用户推荐,聊天 - 支撑子域:客服,反垃圾 - 通用子域:消息推送

限界上下文Bounded Context

限界上下文

限界指划分边界,上下文对应一个聚合,限界上下文可以理解为业务的边界。
一个子域对应一个或多个限界上下文。如果对应多个上下文,则可以考虑子域是否要再进行细粒度的拆分。

限界上下文的目的是为了更加明确领域模型的职责和范围

划分限界上下文

三个原则: - 概念相同,含义不同(通用语言):如果一个模型在一个上下文里面有歧义,那有歧义的地方就是边界所在,应该把它们拆到不同的限界上下文中。 - 外部系统:有时候系统需要同外部系统交互,这时可以把与外部系统交互的那部分拆分出去以实现更好的扩展性。这样一旦外部系统发生了变化,就不会影响到我们的核心业务逻辑。 - 组织扩展:尽量不要两个团队一起在一个限界上下文里面开发,因为这样可能会存在沟通不顺畅、集成困难等问题。

组织架构

康威定律
任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。

团队结构就是组织结构,限界上下文就是系统的业务结构。所以,团队结构应该尽量和限界上下文保持一致。

举例
社交领域中,订单子域对应订单上下文

上下文映射

从宏观上看每个上下文之间的关系,可以更好理解各个上下文之间的依赖关系。

梳理清楚上下文之间的关系是为了: - 任务更好拆分,一个开发人员可以全身心的投入到相关的一个单独的上下文中 - 沟通更加顺畅,一个上下文可以明确自己对其他上下文的依赖关系,从而使得团队内开发直接更好的对接 - 每个团队在它的上下文中能够更加明确自己领域内的概念,因为上下文是领域的解系统

举例
聊天上下文依赖消息推送,推广上下文也依赖消息推送

战术建模

战术建模是从微观角度对上下文进行建模。
梳理清楚聚合根,实体,值对象,领域服务,领域事件,资源库等。

实体Entity

当一个对象可以由标识进行区分时,这种对象称为实体

和数据库中的实体是不同的,这里的实体是从业务角度进行划分的。

实体: - 具有唯一标识 - 持久化 - 可变

举例
社交中的用户即为实体,可以通过用户唯一的id进行区分。

值对象value object

当一个对象用于对事物进行描述而没有唯一标识时,它被称作值对象。
在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。

例如:年龄,聊天表情符号( 😛: 吐舌 (U+1F61B))

习惯了使用数据库的数据建模后,很容易将所有对象看作实体

聚合根Aggregate Root

聚合是一组相关对象的集合,作为一个整体被外界访问,聚合根是这个聚合的根节点。

聚合由根实体,值对象和实体组成。(聚合根里面有多少个实体,由领域建模决定)

外部对象需要访问聚合内的实体时,只能通过聚合根进行访问,而不能直接访问

举例
一个订单是一个聚合根,订单购买的商品是实体,收货地址是值对象。

领域服务Domain Service

领域服务
一些既不是实体,也不是值对象的范畴的领域行为或操作,可以放到领域服务中。用来处理业务逻辑,协调领域对象来完成相关业务。

例如,有些业务逻辑不适合放到领域对象中,或实体之间的业务协调,这些业务逻辑都可以放到领域服务中。

特征 - 与领域相关的操作如执行一个业务操作过程,但它又并不适合放入实体与值对象中 - 操作是无状态的 - 对领域对象进行转换,或以多个领域对象作为输入进行计算,结果产生一个值对象

当采用微服务架构风格,一切领域逻辑的对外暴露均需要通过领域服务来进行。
如原本由聚合根暴露的业务逻辑也需要依托于领域服务。

举例
必须通过订单领域服务来创建和访问订单

领域事件

领域事件是对领域内发生的活动进行的建模。捕获一些有价值的领域活动事件。

作用 - 解耦:可以通过发布订阅模式,发布领域事件 - 一致性:通过领域事件来达到最终一致性 - 事件溯源

举例
发送聊天消息,这属于一个领域事件;撤回消息,也属于一个领域事件。
推送服务订阅消息事件,然后将消息推送给用户端。这样就解耦了消息服务与推送服务之间的强依赖关系。

资源库Repository

资源库用于保存和获取聚合对象。

领域模型 vs 数据模型
资源库介于领域模型(业务模型)和数据模型(数据库)之间,主要用于聚合对象的持久化和检索。

资源库隔离了领域模型和数据模型,以便上层只需要关注于领域模型而不需要考虑如何进行持久化。

分层架构

把一系列相同的对象进行分类放在同一层,然后根据他们之间的依赖关系再确定上下层次关系。

在实际决策时,我们需要知道各层的职责、意义以及相应的场景;
落实到代码层面时,我们还需要知道各层所包含的具体内容、各层的一些常见的具体策略/模式、层次之间的交互/依赖关系。

DDD经典分层架构

ddd_layers.png
  • 用户接口层(interfaces):处理显示和用户请求,以及一些基本的参数检查,不包括业务逻辑
  • 应用层(application):主要协调领域对象的操作;处理持久化事务、发送消息、安全认证等
  • 领域层(domain):处理核心业务逻辑,不包括技术实现细节。领域层是业务软件的核心
  • 基础设施层(infrastructure):处理纯技术细节,为其他层提供技术支撑,也可用于封装调用的外部系统细节。例如:持久化的实现,消息中间件的实现,工具类,rpc等

个人理解:这种分层,既可以在一个单体应用中,也可以是微服务的形式。DDD分层并不一定要按微服务的服务粒度进行分层。
如果一个业务逻辑非常简单的子域,则可以将几层都放进一个单体应用中,在应用中进行分层。如果业务较为复杂,则可以按服务进行拆分,每层都有自己对应的服务。

其他架构

  • 对称性架构
  • 洋葱架构
  • 整洁架构
  • CQRS架构

DDD工程实践

以一个简化的社交领域的例子来实践DDD。

核心概念

  • 用户(User): 一个账户,并以用户id识别
  • 关系(Relationship):用户之间的关系
  • 动态(Feed): 用户发布文字,图片,视频,评论等内容
  • 会话(Conversation):用户之间的聊天会话

领域设计

战略建模

领域就是社交领域,核心问题和绝大部分社交系统一样。

子域
- 核心域:聊天,动态 - 支撑子域:反作弊,推广 - 通用子域:用户,关系,消息推送

上下文

  • 消息上下文
  • 会话上下文
  • 动态上下文
  • 推送上下文
  • 用户上下文

战术建模

以会话上下文为例子来进行战术建模

会话上下文

  • 会话:聚合根
    • 用户:实体
    • 用户:实体
    • 消息列表:实体
      • 发送人:实体
      • 接收人:实体
      • 消息内容:值对象

消息在会话上下文属于实体,在消息上下文属于聚合根。

结构

以会话子域为例

架构分层

  • interfaces 接口层
    • RESTful
    • RPC
  • application 应用层
    • Conversation
    • Message
  • domain_service 领域服务层
    • model
      • Conversation
      • Message
    • repository
      • Conversation
      • Message
  • infrastructure 基础设施层
    • store
      • ConversationRepository
      • MessageRepository
    • message
      • SendMessage
    • utils

领域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package domain

// 聚合根
type Conversation struct {
ID int
User1 User
User2 User
Messages list.List
}

// 实体
type Message struct {
ID int
From User // 实体
To User
Body Content // 值对象
}

用户接口层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type ChatInferface struct {
// 应用层
app app.ChatApplication
}

func (c *ChatInferface) Route() {
c.route("POST", "/api/message", c.SendMessage)
c.route("PATCH", "/api/message", c.RecallMessage)
}

// POST /api/message
func (c *ChatInferface) SendMessage(ctx *Context) {
if !c.validateRequest(ctx) {
return
}
message := c.parseMessage(ctx)
app.SendMessage(message)
}

func (c *ChatInferface) RecallMessage(ctx *Context) {
if !c.validateRequest(ctx) {
return
}
messageID := c.parseMessage(ctx)
app.RecallMessage(messageID)
}

应用层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type ChatApplication struct {
user service.UserService
chat service.ChatService
// 这里领域事件由应用层发布
// publisher EventPublisher
lbs LBSFacade
}

func (c *ChatApplication) SendMessage(msg *Message) {
if !c.user.CheckUser(msg.UserID) {
return
}
c.chat.SendMessage(msg)
}

领域服务层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ChatService struct {
// 领域事件
publisher MessageEventPublisher
repo MessageRepository
}

func (c *ChatService SendMessage(msg *Message) {
// 业务逻辑
...

// 领域资源持久化
c.repo.Save(msg)

// 发布领域事件
c.publisher.Publish(msg)
}

基础设施层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package infrastructure

type MessageRepository struct {
db MessageDatabase
cache MessageCache
}

func (m *MessageRepository) Save(msg *Message) {
db.Save(m.ToPO(msg))
}

func (m MessageRepository) Get(msgID int) *Message {
msg := m.cache.Get(msgID)
if msg != nil {
return m.FromPO(msg)
}
return m.FromPO(m.db.Get(msgID))
}

总结

在设计和实现一个系统的时候,这个系统所要处理问题的领域专家和开发人员以一套统一语言进行协作,共同完成该领域模型的构建,在这个过程中,业务架构和系统架构等问题都得到了解决,之后将领域模型中关于系统架构的主体映射为实现代码,完成系统的实现落地。

Donate comment here
原有内容