领域驱动设计(Domain-Driven-Design)是一种针对大型复杂系统的领域建模与分析方法论。
2003 年,Eric Evans 发布《Domain-Driven Design: Tackling Complexity in the Heart of Software》(领域驱动设计:软件核心复杂性应对之道),其中定义了DDD。
DDD改变了传统软件开发针对数据库进行的建模方法;DDD先对业务领域进行分析,建立领域模型,根据领域模型驱动代码设计。合理运用面向对象的高内聚低耦合设计要素,降低整个系统的业务复杂性,并使得系统具有更好的扩展性,更好的应对多变的业务需求。
一个领域就是一个问题域,只要是同一个领域,那问题域就相同。
只要确定了系统所属的领域,那么这个系统的核心业务,即要解决的问题以及问题的边界就基本确定了。
举例
陌生人社交领域,包含有聊天,用户推荐,朋友圈等核心环节。 只要是这个领域,一般都会有这些相同的核心业务,因为他们要解决问题的本质是一样的,就是交友。
每一个领域,都有一个对应的领域模型,领域模型能够很好的帮我们解决复杂的业务问题。
在DDD中,以领域(domain)为边界,分析领域的核心问题,再设计对应的领域模型,最后通过领域模型驱动代码设计的实现。这样设计的系统才有合理的分层与解耦,对以后业务的迭代开发,代码的维护才更加容易。
然而很多互联网公司,为了追求快速的上线,都是模型都没有想清楚就开始写代码,这就导致了后续代码维护困难,无法扩展。修改bug同时又引入新的bug,反反复复,进入恶性循环。
当然,这跟梳理清楚领域模型需要一定时间,这与初创型的互联网公司需求快速上线有点相悖,但是,这点时间的投入是非常值得的。因为可以避免了系统上线后不久又得重构的问题。
随着产品不断的迭代,业务逻辑变得越来越复杂,系统也越来越庞大。模块彼此互相关联、耦合。导致增加或修改一个功能变得异常艰难,同时功能间的界限也变得模糊,职责不再清晰。这个时候就需要进行重构,拆分。
虽然架构本身是随着业务进行不断演进的;但是,如果架构初始设计不体现出业务的模型,那么新需求就无法体现在现有架构上,导致不断腐化,不断重构。
domain object仅用作数据载体,而没有行为和动作的领域对象。
指领域对象里只有get和set方法,没有相关领域对象的业务逻辑。业务逻辑放在业务层。
将业务逻辑和对象存储放在domain object里面,业务层只是简单进行小部分业务的封装及其他domain的编排。
面向对象设计,符合单一职责设计。
贫血模型的domain object很轻量,这导致业务层的复杂,domain object相关的业务逻辑散布在各个业务层,造成业务逻辑的冗余以及原本domain object的定义就变得相对模糊,这就是贫血症引起的失忆症。
而采用领域开发的方式,将数据和行为封装在一起,与业务对象相映射;领域对象职责清晰,将相关业务聚合到领域对象内部。
DDD 的本质是一种软件设计方法论,而微服务架构是具体的实现方式。微服务架构并没有定义对复杂系统进行分解的具体方法论,而 DDD 正好就是解决方案。
微服务架构强调从业务维度来分治系统的复杂度,而DDD也是同样的着重业务视角。
战略设计就是从宏观角度对领域进行建模。划分出业务的边界,组织架构,系统架构。
DDD中,对系统的划分是基于领域的,也是基于业务的。
通用语言是指确定统一的领域术语,提高开发人员与领域专家之间的沟通效率。
一旦确定了统一语言,无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,清晰准确地定义领域知识。
当确认整个团队统一的语言后,就可以开始进行领域建模。
领域Domain
一个领域本质上可以理解为就是一个问题域。只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。
如果一个领域过于复杂,涉及到的领域概念、业务规则、交互流程太多,导致没办法直接针对这个大的领域进行领域建模。这时就需要将领域进行拆分,本质上就是把大问题拆分为小问题,把一个大的领域划分为了多个小的领域(子域),那最关键的就是要理清每个子域的边界
子域可以根据自身重要性和功能属性划分为三类子域:
每个领域的划分都不一样。对相同领域公司而言,其核心,支撑,通用的子域也可能有不一样的地方,但大体上基本都是一样的。
举例
社交领域的划分
限界指划分边界,上下文对应一个聚合,限界上下文可以理解为业务的边界。
一个子域对应一个或多个限界上下文。如果对应多个上下文,则可以考虑子域是否要再进行细粒度的拆分。
限界上下文的目的是为了更加明确领域模型的职责和范围
三个原则:
康威定律
任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。
团队结构就是组织结构,限界上下文就是系统的业务结构。所以,团队结构应该尽量和限界上下文保持一致。
举例
社交领域中,订单子域对应订单上下文
从宏观上看每个上下文之间的关系,可以更好理解各个上下文之间的依赖关系。
梳理清楚上下文之间的关系是为了:
举例
聊天上下文依赖消息推送,推广上下文也依赖消息推送
战术建模是从微观角度对上下文进行建模。
梳理清楚聚合根,实体,值对象,领域服务,领域事件,资源库等。
当一个对象可以由标识进行区分时,这种对象称为实体
和数据库中的实体是不同的,这里的实体是从业务角度进行划分的。
实体:
举例
社交中的用户即为实体,可以通过用户唯一的id进行区分。
当一个对象用于对事物进行描述而没有唯一标识时,它被称作值对象。
在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。
例如:年龄,聊天表情符号( :stuck_out_tongue:: 吐舌 (U+1F61B))
习惯了使用数据库的数据建模后,很容易将所有对象看作实体
聚合是一组相关对象的集合,作为一个整体被外界访问,聚合根是这个聚合的根节点。
聚合由根实体,值对象和实体组成。(聚合根里面有多少个实体,由领域建模决定)
外部对象需要访问聚合内的实体时,只能通过聚合根进行访问,而不能直接访问
举例
一个订单是一个聚合根,订单购买的商品是实体,收货地址是值对象。
领域服务
一些既不是实体,也不是值对象的范畴的领域行为或操作,可以放到领域服务中。用来处理业务逻辑,协调领域对象来完成相关业务。
例如,有些业务逻辑不适合放到领域对象中,或实体之间的业务协调,这些业务逻辑都可以放到领域服务中。
当采用微服务架构风格,一切领域逻辑的对外暴露均需要通过领域服务来进行。
如原本由聚合根暴露的业务逻辑也需要依托于领域服务。
举例
必须通过订单领域服务来创建和访问订单
领域事件是对领域内发生的活动进行的建模。捕获一些有价值的领域活动事件。
举例
发送聊天消息,这属于一个领域事件;撤回消息,也属于一个领域事件。
推送服务订阅消息事件,然后将消息推送给用户端。这样就解耦了消息服务与推送服务之间的强依赖关系。
资源库用于保存和获取聚合对象。
领域模型 vs 数据模型
资源库介于领域模型(业务模型)和数据模型(数据库)之间,主要用于聚合对象的持久化和检索。
资源库隔离了领域模型和数据模型,以便上层只需要关注于领域模型而不需要考虑如何进行持久化。
把一系列相同的对象进行分类放在同一层,然后根据他们之间的依赖关系再确定上下层次关系。
在实际决策时,我们需要知道各层的职责、意义以及相应的场景;
落实到代码层面时,我们还需要知道各层所包含的具体内容、各层的一些常见的具体策略/模式、层次之间的交互/依赖关系。
个人理解:这种分层,既可以在一个单体应用中,也可以是微服务的形式。DDD分层并不一定要按微服务的服务粒度进行分层。
如果一个业务逻辑非常简单的子域,则可以将几层都放进一个单体应用中,在应用中进行分层。如果业务较为复杂,则可以按服务进行拆分,每层都有自己对应的服务。
以一个简化的社交领域的例子来实践DDD。
领域就是社交领域,核心问题和绝大部分社交系统一样。
以会话上下文为例子来进行战术建模
消息在会话上下文属于实体,在消息上下文属于聚合根。
以会话子域为例
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 // 值对象 } 复制代码
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) } 复制代码
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) } 复制代码
type ChatService struct { // 领域事件 publisher MessageEventPublisher repo MessageRepository } func (c *ChatService SendMessage(msg *Message) { // 业务逻辑 ... // 领域资源持久化 c.repo.Save(msg) // 发布领域事件 c.publisher.Publish(msg) } 复制代码
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)) } 复制代码
在设计和实现一个系统的时候,这个系统所要处理问题的领域专家和开发人员以一套统一语言进行协作,共同完成该领域模型的构建,在这个过程中,业务架构和系统架构等问题都得到了解决,之后将领域模型中关于系统架构的主体映射为实现代码,完成系统的实现落地。