在传统的 MVC 架构下开发时,通常采用的是 "数据驱动设计"(自底向上设计开发):根据需求先建立数据库表,将数据库表映射为持久化对象(PO),然后在服务层通过 CRUD 操作进行 过程式编程 ,这导致模型(贫血模型)无法直观地反映业务实际情况。在多个场景下出现了功能相似但又有所不同的需求时,经常导致重复编写相似的代码。同时,由于缺乏对业务领域的深入理解和沉淀,服务间的调用往往缺乏清晰的结构,导致逻辑交织在一起,这不仅降低了系统的可读性,也给系统的可维护性带来了挑战。最终造成了 逻辑上的分散,系统整体的内聚性不足。但是这种架构设计非常简单,能在小规模团队下完成快速迭代和交付。
领域驱动设计(DDD)是一种软件设计方法,那么该如何理解领域驱动设计呢?我们可以将其分解为"领域"和"驱动设计"两个部分来理解:
- 领域 ,它由三部分组成:业务范围内的用户,即 涉众域 ;用户要实现某种业务价值,解决某些痛点或实现某种诉求,即 问题域 ;面对业务价值,痛点和诉求,有对应的解决方案,这是 解决方案域
- "驱动设计"有两层含义:一是业务问题域驱动领域建模的设计过程 ;二是领域模型驱动技术实现或代码开发的设计过程。领域模型划分的准确性是关键,因为它可以保证代码实现能够真实反映并解决业务的核心问题
领域驱动设计通俗地讲就是:针对特定业务,用户在面对业务问题时有对应的解决方案,这些问题与方案构成了领域知识,它包含流程、规则以及处理问题的方法,领域驱动设计就是围绕这些知识来设计系统。它从 业务 出发,区别于 "数据驱动设计",自顶向下设计系统 ,围绕业务概念构建领域模型来控制业务的复杂性 ,主要用于 处理复杂业务需求。
以营销为例,营销系统所服务的用户有 4 类:运营、销售、电销人员和商户。解决 3 个核心问题:如何发券、发给谁、发什么(红包还是折扣券)。解决方案:通过营销活动来承载发券,不同的活动类型对应不同的玩法(如买赠、折扣、充送等);通过目标人群来确定发给谁;通过权益来定义发什么(如:红包、代金券、折扣券等)。如下图所示:

领域驱动设计的过程
领域驱动设计需要业务、产品和研发共同参与,可以从三个步骤来执行领域驱动设计的落地:
- 战略设计 :确定用例 ,统一语言 和 划分子域,统一语言会贯穿领域驱动设计从战略设计到战术设计到最后的代码实现全过程,对于需求分析、知识提炼和最后代码的实现,都非常重要
- 战术设计:概念模型转化成类(代码)模型
- 代码架构:将系统设计映射为系统实现

战略设计
确定用例
战略设计之前,先要确定用例,也就是业务是怎么玩的,有几种常见的方法:
- 用例图:最简单直观的表达了用户与系统的交互
- 用户故事:敏捷开发模式下用的较多,从Who、What和Why三个维度描述了业务需求
- 交互原型:用户操作的页面及其操作流程,其缺点是过于关注用户体验,而忽略了业务底层逻辑
- 事件风暴:关注业务的底层逻辑,但使用门槛较高,适用于大型而复杂的业务分析

以美团营销系统的用例图为例:

统一语言
确定业务玩法后,接下来是 统一语言 。从用例里抽取概念,并对概念进行甄别(去伪存真,抽象合并)找到真正描述业务的概念。比如,有多种方式来描述活动规则:充值送规则、返还规则和档位等,技术可能会泛泛地称其为规则,业务人员则用档位来描述(比如充值送活动,充1000送100红包,充2000送300红包,充3000送500红包,那1000、2000、3000就是业务所认为的档位)。抽取概念时,尽量采纳业务、产品侧的叫法,这样统一语言比较容易推行,也有助于团队成员之间的沟通,便于后续的设计和开发工作:

接着是明确概念的含义,概念由术语、Term(术语的英文版)和含义三部分构成。含义明确的术语就是统一语言,这些术语将用在日常需求沟通、产品文档,技术设计以及代码实现中:

抽象概念模型
明确概念后,接着理清概念之间的关系(1对1,多对1,多对多),确定概念所代表的的业务实体的核心属性和行为,从而得到概念模型。后续在业务需求讨论、产品和技术方案设计时,基于这个概念模型,使用统一语言进行描述,大家能很容易对齐;同时精心抽出的概念和建立的概念模型更接近业务本质,为后续的战术设计打下了基础。

基于统一语言和概念模型,业务、产品和技术三个角色比较容易就需求达成共识,保障后续沟通的一致性。如果缺少这些就很容易出问题,如:刚开始做营销系统时,在如何描述"商户"上,没有统一语言,资金域有三个概念来描述商户(资金账户、账号ID、资金账号),商家域有四个概念描述商户(商家账号、商家ID、登录号、登录ID),到了营销域,不同的人采用不同的概念来描述商户,造成了沟通的混乱。给商户发红包时,"资金账户、账号ID、资金账号、商家账号、商家ID、登录号、登录ID"这些概念都可以描述商户,但业务人员弄不清这些概念之间的区别,导致ID误用,红包发错。事后对这些概念进行了梳理和统一,营销域只关注资金账户和商家账号,系统功能上明确使用资金账户或商家账号来发送红包,这样就不易出错了。

划分子域
现在我们已经有了概念模型,但是它现在还是一张大网,仅仅描述了概念间的关系以及关键属性,但还不能直接映射为代码模型,要 映射为代码模型,还需拆解,化繁为简。可以从以下三个方面进行拆解:
- 基于涉众域拆解:也就是按用户相关性进行拆解,不同的用户使用不同的系统功能,如:CRM由市场人员、销售人员、客服人员三类角色协同完成客户触达,签约合作,售后服务三大职能,针对这三个角色建设相应的系统能力。这种拆解方式比较简单,但也存在较大的局限性,可能导致功能的重复建设
- 基于问题域拆解:不同角色/用户要解决的问题是相同/相似的,可基于问题域进行拆解,如营销系统的用户包括销售、商户、销运等角色,但它核心是要解决如何发券(活动),发给谁(人群),发什么(权益)的问题。基于问题域的拆解相较于基于涉众域的拆解更加抽象,但也可能复用性不够
- 基于解决方案域拆解:不同的问题,可能有相同的解决方案,如HR域有请假审批、财务上有报销流程、CRM领域存在客户资质审批,三个领域各自需要解决审批流程的问题,可以构建通用的审批流引擎来统一解决,这是基于解决方案域进行拆解。基于解决方案域的拆解最抽象,也最贴合业务本质,但也容易陷入过度设计的陷阱

营销系统基于问题域拆解为五个子域(活动域,权益域,人群域,推送域,数据域),每个子域解决特定的问题,各子领域相对内聚和简单:

通过以上这些分析步骤确保了即使是不熟悉领域的团队成员,也能够准确地理解业务并作出恰当的架构决策。实际上,对于有经验的架构师而言,确实可以迅速地完成领域的识别和划分,这也展示了领域驱动设计过程中的一种艺术性。
上下文映射
业务系统要运转起来,需要子域之间相互配合,这就要定义 上下文映射(Context Mapping) ,实现不同子域间的协作。如活动域关注的两个目标人群:一是资金账户(表示已签约的商户);另一个是商家账号(表示未签约商户)。资金账户是财务域定义的,而商家账号是账号域定义的,两个概念都不是营销域原生概念。此时,营销域需通过某种方式依赖外部概念,将外部概念映射到营销域,通过 防腐层(ACL: Anti-Corruption Layer) 来对接外部服务来实现这种映射。如下所示:

领域驱动设计里定义九种上下游映射关系,上下文映射是用于定义和管理不同限界上下文(Bounded Context)之间关系的一种工具。以下是 DDD 中常见的九种上下文映射关系:
- 共享内核(Shared Kernel):两个或多个团队共享一部分共同的模型。这部分模型必须非常稳定,因为任何变更都需要所有相关团队的同意
- 客户-供应商(Customer-Supplier):一个上下文(供应商)为另一个上下文(客户)提供服务。客户上下文对供应商上下文有影响力,可以要求变更以满足其需求
- 合作伙伴(Partnership):两个或多个上下文密切合作,共同开发和演化模型。双方对彼此有很强的依赖关系,需要紧密协调
- 保留客户(Conformist):一个上下文必须完全接受另一个上下文的模型和设计决策,而没有对其进行调整或更改的权力
- 防腐层(Anti-Corruption Layer):使用适配层来隔离和翻译外部系统的模型,以保护自身的领域模型不被外部复杂性影响
- 开放主机服务(Open Host Service):一个上下文通过一组开放的、已发布的接口向其他上下文提供服务。这些接口被视为稳定和可靠的
- 发布语言(Published Language):定义一个通用的语言或协议,用于在不同上下文之间进行通信和数据交换
- 独立方式(Separate Ways):上下文之间没有交集或依赖关系,彼此独立发展。在这种情况下,两个上下文之间没有必要的集成
- 大泥球(Big Ball of Mud):这种关系通常是反模式,表示系统中没有清晰的边界和结构,导致上下文之间的混乱和耦合
这些上下文映射关系帮助团队在复杂系统中明确和管理不同子系统之间的交互方式,确保系统架构的稳定性和灵活性。下图是营销系统的整体上下文关系:

一个很形象的隐喻:细胞质所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。
持续迭代
从用例分析,统一语言到子域拆分,初步完成战略设计,但这并非终局,战略设计是一个持续迭代的过程,迭代的来源主要有3个:
- 用例精化:在探讨需求的过程中,用例不断丰富
- 需求变更:业务不断发展带来需求变化,进而影响用例及相关概念的内涵,概念模型亦随之调整和迭代
- 方案选型:当产品,业务或技术发生较大变化时,可能需要采用另一种方式实现它,这时所采用的概念会有所不同
比如早期构建营销活动域时,通过参与规则来定义谁可以参加活动,将商户与参与规则进行匹配,符合就能参与。这种方式带来的问题是无法提供一个完整的活动人群列表,除非将所有商户(5000万+)匹配一遍。随着业务方越来越重视活动参与商户的分层,触达和转化,引入目标人群的概念,通过目标人群来保存所有可参加活动的商户。从参与规则到目标人群,概念发生了变化,底层模型也完全不一样(参与规则是一套规则体系,而目标人群由筛选服务提供),实现了战略设计上的迭代。
有了战略设计,构建了统一语言和概念模型后,如何验证概念模型呢?通常用两个方法:
- 场景走查:把模型代入到所有的场景确认一遍,确定所抽象出来的概念模型和统一语言能正确描述它
- 业务预判:未来业务的变化会在哪里,当变化发生时,概念模型的内涵和外延是否方便扩展并支持到变化

战术设计实践
战略设计得到了概念模型,战术设计则是 将概念模型映射为代码模型:
- 首先,概念是分层的,如营销活动是一个泛化概念,其下还有充值送活动、消费返活动,买赠活动等具体活动。构建对象模型时,通过派生/继承来实现概念分层
- 其次,概念关系映射成对象关系,比如营销活动包含了档位和库存,那在构建营销活动对象时,可通过组合实现这种包含关系(档位对象和库存对象成为营销活动对象的属性)
- 最后,概念的属性行为,可以直接变成对象的属性和行为;概念的状态机以及生命周期也会变成对象的状态机;实体和值 对象这两者的 区别是是否有统一标识和自己的状态

有了对象模型,还需通过聚合根完成封装,如何确定聚合根的粒度?营销活动包含活动、库存、档位、档位项、目标人群五个对象,如果采用小聚合根模式,一个对象对应一个聚合根,这样每个聚合根都很简单。但从业务角度看,库存或档位会影响活动的状态,如:修改了库存或档位,活动需要重新审批和上下线,这种业务上的耦合需要在技术上进行处理。此时,就得在小聚合根上构建领域服务来封装这些逻辑。
另外一种模式是大聚合根。围绕活动,把活动相关的概念(活动、库存、档位、档位项、目标人群)都封装起来,但聚合根比较复杂,影响活动加载(一些活动的目标人群上百万,懒加载可解决问题,但增加了复杂度)。
聚合根的设计要遵循一定的原则:
- 满足业务一致性、数据完整性、状态一致性。比如库存档位和活动状态要一致,在数据上也要完整,不存在没有档位的活动,也不存在没有库存的活动
- 技术限制。有些实体会带来技术挑战,如数据量太大,可抽出来单独考虑
- 业务逻辑不灭,在业务封装与适度的职责边界之间寻找平衡。不管是大聚合根还是小聚合根,业务逻辑永远都是存在的,就是看把它放在哪里

如下图是营销系统的聚合根:

领域驱动设计采用 充血模型 ,"聚合" 具有行为,这些行为都是 "业务知识" 的体现,相当于代码层面的文档。需要注意 不适合放在聚合根里的领域逻辑,可以放在领域服务里,如:同时存在多个充值送活动时,用户只能参加优先级最高的一个,在充值送活动聚合根里会标识活动的优先级,但挑选优先级最高的活动并非聚合根的职责,但确实是领域逻辑的一部分,此时可通过领域服务实现。
从概念模型,类模型到代码实现,整个过程都要使用统一语言 。在落地代码时,代码要体现出业务含义,比如下图的例子,要避免左边 updateStatus()
这样的方法,它没有体现业务含义(必须阅读代码实现,才知道这个方法做了什么);图中右边的 submitCampaign()
,approveCampaign()
,cancelCampaign()
则有明确的业务含义。

代码架构实践

代码架构各个分层的职责我会在领域驱动设计实践的文章中去详细解释。
总结
- 我们做的大部分系统都不是全新系统,如CRM、HR或SCM等,已经有很多业界实践,可充分借鉴这些实践,没必要自己创造新概念
- 要重视统一语言。在聊需求的那一刻,设计就开始了,统一语言就是设计的一部分,没有统一语言就不会有概念模型,没有概念模型就不可能有靠谱的代码模型,拿到需求后就开始设计代码模型是不靠谱的
- 领域驱动设计是团队工作。现实中没有一个是严格意义上的领域专家,所有参与到这项工作的人都可以是领域专家,整个工作可以由技术团队主导,但一定要落地到产品和业务
- 拥抱变化,持续迭代。模型是相对稳定的,但并非一成不变,业务理解的深度,抽象的角度与方式,业务的变化都会影响到领域模型,领域模型的建立是持续迭代的过程
这里分享几个常见的误区:
- 深陷领域驱动设计的概念体系。在代码里生搬硬套领域驱动设计里的概念,比如聚合根、值对象、实体等,掰扯概念之间的细微差异,设计复杂的领域事件等。这反而增加理解成本,让系统变得复杂。领域驱动的精髓在于从业务出发,抽象出业务领域知识,构建概念模型,一步一步将这些概念模型映射成系统。至于如何采用聚合根、领域服务、实体、值对象、领域事件等,可以灵活取舍
- 试图通过精心设计来获得领域模型。领域模型不是设计出来的,而是通过战略设计的几个步骤,从业务中抽象出来的,最重要是理解业务,对业务进行抽象
- 使用了DDD就一定会产生好的领域模型的想法也不可取,我们知道飞机怎么造,但我们不一定能够造出好飞机,但如果我们知道这个方法,可以少走弯路
解决方案域在模型维度分为四层:
- 功能模型:产品表达给我们业务的玩法,我们把它变成了用例,从用例里抽取出功能模型
- 概念模型:对功能模型进一步抽象,统一语言,形成概念模型
- 代码模型:将概念模型映射为代码模型
- 数据模型:业务数据需要存储,需要设计对应的表结构
这里有两个陷阱:
- 看到功能模型后,就开始设计数据模型,考虑数据该怎么创建、怎么更新、什么时候该删除,沦落为CRUD boy
- 看到功能模型后,就开始考虑操作数据的流程是什么,陷入到事务脚本陷阱(对于一些简单的功能,不排斥使用事务脚本,但对于复杂功能,事务脚本的维护成本非常大)
此外,领域至少可以分为两大类:一是学科型,比如财务、会计、图形学、动力学,这类系统的设计须先深入理解学科知识;二是实践型,如CRM、订单交易等,是业务经验的总结,这类系统的设计不妨参考前人的实践。当然,如果自己的业务具有独特性,那就只能靠自己摸索了。
巨人的肩膀
- 《领域驱动设计:软件核心复杂性的应对之道》
- 美团技术团队 - DDD在大众点评交易系统演进中的应用
- 美团技术团队 - 领域驱动设计DDD在B端营销系统的实践