领域驱动设计(DDD)
1 通用类概念
1.1 领域驱动设计(DDD)
DDD 是 Domain-Driven Design 的缩写,是 Eric Evans 于 2004 年提出的一种软件设计方法和理念。
其主要的思想是,利用确定的业务模型来指导业务与应用的设计和实现。主张开发人员与业务人员持续地沟通和模型的持续迭代式演化,以保证业务模型与代码实现的一致性,从而实现有效管理业务复杂度,优化软件设计的目的。
白话例子: 比如你要做一个「外卖平台」,DDD 的做法不是程序员自己猜:这里建一个 $$orders$$ 表、那边建个 $$users$$ 表就开干,而是先跟运营、商户经理、配送负责人一起把「下单、接单、出餐、配送、收餐、退款」这些业务过程讲清楚、画成模型。之后代码里的类名、方法名也用这些业务词汇表示(如 $$Order$$、$$DeliveryAssignment$$),这样一来:业务一变,大家先改模型,再改代码,业务和代码始终对得上,不会出现「嘴上说已经有预订单功能了,代码里根本没有对应结构」的情况。
1.2 模型(Model)
通常,模型是对对象、人或系统的信息表示。它通过较为简单的信息结构来代表我们需要理解的复杂事物或系统。
地图、乐高、算筹都是模型,模型可以简化复杂事务的认知。我们天生就有用简单的东西代表另外一个东西的能力,比如幼儿园数数用的竹签,学习物理时的刚体、真空中的球形鸡,都是模型。通俗来说模型就是经验的抽象集合,平时听到的谚语、公式、定理,本质上都是一种模型。
白话例子: 现实里的「外卖订单」有很多细节:顾客是谁、在哪、商家做菜要多久、骑手位置……但在系统里,你可能用一个 $$Order$$ 类,里面只有「收货地址、菜品列表、总价、状态」这些字段,这个 $$Order$$ 就是对「现实订单」的一个模型,好比用地铁线路图来表示整个城市复杂的交通,只保留和出行有关的那条线和站点。
1.3 建模(Modeling)
建模是构建模型的过程。
在软件设计过程中,通过分析业务,将业务需求使用合适的模型表示出来,是建模的任务。模型可以作为业务分析的产出,并作为软件设计的重要理论基础。 比如在分析一个电商应用的业务时,抽象出订单、商品等概念,进一步定义出模型,并用合适的图例表达,往往是 UML 来表达。
白话例子: 你和电商运营聊天,他们说:「顾客可以把商品放进购物车结算,订单分为待支付、已支付、已发货、已收货。」你听完之后,在白板上画出 $$Cart$$、$$Order$$、$$Product$$ 这几个框,标清楚关系和状态流转,这个「从嘴巴里的业务 → 图上的框和箭头」的过程,就是建模。等模型稳定了,你再照着这些框去写类、写接口。
1.4 模型驱动设计(Model-Driven Design)
面向模型的分析设计方法,优先通过识别模型来简化业务设计。
在设计过程中,以模型为中心,始终维护模型,并基于此指导软件设计。
白话例子: 做一个会员系统,常见做法是先「拍脑袋」写接口:$$/addUser$$、$$/setLevel$$……然后慢慢补字段。模型驱动设计则是:先搞清楚有哪几类核心概念——「会员、等级、积分规则、权益」,把这些抽成模型,写清楚关系和规则,再反推接口和数据库结构。以后比如要增加「生日双倍积分」,你先在积分规则模型上扩展一个「触发条件=生日」而不是随手在某个 Controller 里塞一段 if 逻辑。
1.5 软件设计(The Software Design)
软件设计软件需求出发,对软件系统的整体结构、模块做出划分和规划,以便于具体代码的顺利编写。
由于软件需求具有非结构化、准确的语义,软件设计往往通过经验完成,无法通过某种特定的推理路线严格推导实现。
白话例子: 一个老板说「我要一个可以下单、查账、发工资的系统」,这句话本身很模糊,不可能直接翻译成代码。你需要凭经验先把系统分成「订单模块、结算模块、薪资模块」,各模块怎么互动、放在哪台机器上、怎么部署,这一整套从「一句模糊需求」变成「清晰结构和模块划分」的过程,就是软件设计。
1.6 战略设计(Strategic Design)
战略设计也称为战略建模,是指对业务进行高层次的抽象和归类。主要手段包括理清上下文和进行子域的划分。
战略设计关注模型的分离,其解决的问题是大的模型如何划分为小模型以及相互之间如何关联。战略设计的产出可以用于指导团队协作,使得规模巨大的软件可以被合理拆分。
战略设计的产出通常为上下文图,以及模块或微服务划分。
白话例子: 做一个「综合电商平台」,里面有:商品管理、下单支付、仓储物流、客服工单等。战略设计阶段,你不会管「订单上有没有备注字段」,而是先决定:订单支付是一块上下文、仓储物流是一块、客服是一块,哪些团队负责哪块,它们通过什么接口或消息交互。就像盖商场时先决定「哪些是餐饮区、哪些是服装区、哪里是停车场」,而不是一开始就讨论「这家店挂几盏灯」。
1.7 战术设计(Tactical design)
战术设计也称为战术建模,是指对特定上下文下的模型进行详细设计。
战术设计的对象包括聚合、实体和值对象,其目标是用明确它们是什么以及相互之间有何关系。战术设计的产出可以是用 UML 表达的类图,需要细化到具体的属性,同时确保在代码级别可实现。
白话例子: 当你确定「订单系统」是一个独立上下文之后,战术设计就深入到这块内部:一个订单(实体)里有哪些值对象(收货地址、金额),订单和订单项怎么组织成一个聚合,取消订单有哪些规则,哪些字段要参与事务。就像你已经决定「这里是餐饮区」,接下来开始精细到「这家店厨房多大、吧台怎么摆、动线怎么走」。
1.8 软件(Software)
DDD 讨论下的软件是指,用于解决具体业务问题的计算机程序,既可以是单体也可以是分布式系统。
软件设计是 DDD 的最终目的,使用 DDD 的各种工具可以指导软件设计,最终构建出健壮、容易维护的软件。
白话例子: 比如一个「报销审批系统」,不管它是一台服务器上跑的单体应用,还是拆成报销服务、审批服务、通知服务多个微服务,对 DDD 来说都是「软件」。DDD 关心的是:你如何用业务概念建模、分层、拆服务,让这个系统不至于过两年变成谁都不敢改的一团糟。
1.9 原则(Principle)
为了更好的践行 DDD,需要遵守几个原则: 通用语言、聚焦核心域、协作共创和持续建模。
这些原则是为了更好地服务业务,从业务驱动模型设计。
白话例子: 假设你在做一个「在线教育平台」,如果大家都坚持用同一套术语(通用语言)、把精力放在课程和学习路径这些关键业务上(聚焦核心域)、经常和教研、运营一起开会共创(协作共创)、业务一变就立刻更新模型和代码(持续建模),这个系统一般不会太跑偏;反过来,这些原则一旦被忽略,系统就容易越做越像「补丁大礼包」。
1.10 通用语言(Ubiquitous)
通用语言(Ubiquitous language)是指在软件设计中,业务人员和开发人员需要使用无歧义的统一语言来对话。
这些语言包括对概念的统一理解和定义,以及业务人员参与到软件建模中,否则业务的变化会造成软件巨大的变化。
白话例子: 在一个电商公司里,如果产品说「店铺」,而开发有的人叫「商户」、有的人叫「卖家账号」,数据库里又叫 $$vendor$$,久而久之每个人理解都不一样,改一个功能容易「动错地方」。通用语言要求大家统一叫「商家」,文档、接口、数据库都用同一个词,这样讨论「商家冻结」时,全体都知道谈的是哪个概念和哪张表。
1.11 聚焦核心域(Focus)
核心域就是最关键的业务逻辑,聚焦核心域决定了软件的定位和投资重心。
白话例子: 如果你做的是一个「信用评分」SaaS,真正核心的是评分模型和风控决策;登录页面长什么样、短信发哪个供应商都不是关键。聚焦核心域意味着:你把最好的架构师和时间用在「评分规则建模」这块,短信可以直接用云厂商 SDK,登录可以用成熟组件,不要在非核心功能上耗尽精力。
1.12 协作共创(Collaboration)
协作共创是指领域专家和业务专家共同建模。
白话例子: 比如你要做一个「医院挂号系统」,如果只有程序员自己拍脑袋设计,很可能漏掉很多预约限制(专家号配额、急诊插队、跨院转诊)。协作共创就是把医生代表、挂号窗口负责人、信息科一起拉来开工作坊,大家一边讲流程,一边把规则写到模型里,而不是技术人员闭门造车。
1.13 持续建模(Continuous)
持续建模是指模型需要随业务变化而被及时更新。
白话例子: 原来你的外卖平台只做「普通订单」,某天业务说要加「预约下单」和「团购拼单」,如果只是在 Controller 里硬写几个 if,把新逻辑糊上去,模型还是老样子,很快就会乱。持续建模的意思是:业务一有新玩法,就回到模型层面补全概念(比如加「拼单订单类型」「预约送达时间」),再让代码跟着一起演进。
1.14 上下文(Context)
上下文是语言学科的概念,指不同语境下的概念虽然相同的用词,可能具有不同的含义。
在软件设计中,因为自然语言的原因,相同的用词导致实际是不同概念,会对建模和软件设计带来误导。同时,不同的上下文也是识别模型边界的手段。
白话例子: 「用户」这个词:在支付系统里可能指「付款账户持有人」,在内容系统里可能是「发帖的人」,在客服里则是「打电话进来的人」。如果你在一个大系统中到处使用同一个 $$User$$ 实体,很快就会发现字段一堆、含义冲突。把「支付上下文」「内容上下文」「客服上下文」分别划清,就能在每个上下文里给「用户」一个清晰一致的含义。
2 领域分析类概念
2.1 问题空间(Problem Space)
待解决的业务问题的集合。
在 DDD 实践中,我们应该明确区分问题空间和解空间,避免混为一谈。
白话例子: 比如老板说:「我们现在报销慢、审批容易漏单、员工不知道进度」,这些「哪里痛、哪里乱」就是问题空间;此时还没讨论该不该做 App、用不用户登录,这些都是后面解法的问题。区分问题空间的意思是:先把「报销到底有什么痛点」说清楚,再去想用什么系统来解决。
2.2 领域(Domain)
领域(Domain)是业务相关知识的集合。
通俗来说,领域就是业务知识。业务有一些内在规则,存在专业性,比如财务、CRM、OA、电商等不同领域的业务规则不同。计算机只是业务规则的自动化。
白话例子: 「财务记账」是一个领域,有它自己的一套借贷规则、科目结构、税务要求;「电商」是另一个领域,讲的是商品、订单、促销、物流。你写程序之前,先得把这些业务知识学明白——软件只是把这些专业规则用代码固化下来。
2.3 子域(Sub Domain)
一个子域是领域的一部分。
为了降低业务理解复杂度,DDD 实践中通常将领域划分为子域,通过分而治之的方法分析问题。
白话例子: 在「电商领域」内部,还可以分出「商品目录子域」「营销促销子域」「订单履约子域」等。你不用一次性把整个电商搞透,而是可以先专注理解「订单履约」这块:下单、支付、发货、签收,等这块清楚了再扩展到下一块,就像把一个大难题拆成几个可以单独啃的小题。
2.4 核心域(Core Domain)
核心域是指领域中最核心的部分,通常对应企业的核心业务
核心域需要我们投入最大精力,进行充分的分析。因为它是一个企业能运转的基础。
白话例子: 对于美团这样的公司,「到店/到家交易撮合和配送调度」就是核心域;如果这些挂了,美团的根业务就瘫痪了。反之「员工报销系统」只是支撑业务。做架构时,核心域的模型要最精细、测试最齐全、团队最强,其他域可以考虑买现成或简化实现。
2.5 支撑域(Support Domain)
支撑域是一种特殊的子域,是指为了实现核心业务而不得不开发的业务所对应的相关知识的集合。
例如,活动平台业务属于电商的支撑域,因为该业务对于电商企业并不是必需的,其存在的意义仅在于放大利润。
白话例子: 比如电商做「双十一大促」的活动系统,本质上是为了帮「下单卖货」这个核心业务多卖一点。没有活动系统照样能下单,只是卖得没这么猛。所以这是支撑域:对公司很重要,但不是公司存在的根理由。
2.6 通用域(General Domain)
通用域是另一种特殊的子域,对应的是业界已经有成熟方案的业务。
通用域可以看做一种特殊的支撑域,可以使用标准部件来实现,短信通知、邮件等领域问题。
白话例子: 「发送短信验证码」几乎每家公司都会用,但它的商业差异很小,基本都是「接运营商、控制频率、记录日志」。这种领域大家都已经玩得很成熟,你没必要自己从零造一套短信平台,直接用云厂商产品或开源方案即可,这就是通用域。
3 建模类概念
3.1 解空间(Solution Space)
解空间是一个数学概念。是指满足问题的所有约束前提下,所有可行解的集合。在 DDD 的上下文中,指的是所有可能的解决方案的集合。
白话例子:
比如你要从「上海到北京」并且「今天出发,预算不超过 1000 元,还要求当天到达」。所有符合这些条件的出行方案(高铁某几趟、飞机某几班、甚至拼车)放在一起,就是这个出行问题的「解空间」。在做系统设计时,我们不是瞎想解决方案,而是从这些明确约束下的所有可行方案中选最合适的一个。
解空间是相对问题空间存在的,认识到解空间存在的好处是解空间可以通过一些方法从问题空间导出,而不是通过猜测得出的。
白话例子:
「问题空间」就像是你说:我要从 A 到 B,有时间限制、有预算、有舒适度要求等;「解空间」就是在这些要求下所有可能的走法。知道有解空间这个概念后,你会去系统地「枚举和筛选」这些走法(比如用搜索算法、优化算法),而不是拍脑袋选一个你主观觉得还行的路线。
3.2 领域模型(Model)
领域模型(Model)是业务概念在程序中的一种表达方式。
白话例子:
例如,在一个「网店」业务里,有「用户」「商品」「订单」「支付记录」这些概念。程序里就会对应有 User、Product、Order、Payment 等类或结构体,它们带着这些业务概念需要的属性和行为。这些在代码中的类、对象、结构,就是把真实业务概念搬进程序形成的「领域模型」。
领域模型可以用来设计和理解整个软件结构。面向对象设计中的类概念是领域模型的一种表达方式。与此类似,UML 的建模方法也可以应用在对领域模型的表达上。在 DDD 实践中,领域模型应当尽量简洁,能反应业务概念即可。
白话例子:
做系统设计时,你可以先画出「订单有多个订单项」「订单属于某个用户」「订单可以取消、支付」等 UML 图,把这些关系和行为画清楚,再落到代码里变成类和方法。DDD 不鼓励你把一堆技术细节塞进模型里,而是希望你的模型看上去就像业务人员说话那样简单:这个对象代表订单,它能做的事情就是业务里订单能做的事情,别额外搞一堆和业务无关的东西。
3.3 问题域(Problem Domain)
问题域(Problem Domain)是指真实世界中,软件要解决的那一大块业务与问题空间;其中的业务规则与知识称为 业务领域知识。
白话例子:
- 问题域 = “整盘业务到底在干什么”;
- 业务领域知识 = “这门生意的行规和玩法”;
- 子域 = “把一整盘业务拆成一个个好消化的小问题”。
就像做一个“外卖平台”,整个平台是问题域,“用户点单”“商家接单”“骑手配送”就是不同的子域。
3.3 限界上下文(Bounded Context)
限界上下文是有明确边界的上下文。在 DDD 实践中领域模型会被限定在限界上下文当中。
白话例子:
想象一个大型电商平台,整体很大,但我们会划出「商品中心」「交易系统」「支付系统」「仓储物流」这些业务边界。每一块就是一个限界上下文:在「交易系统」里谈的订单,和在「仓储系统」里谈的订单,含义和属性可能不一样,模型也不同,它们各有自己的「小世界」。
限界上下文强调概念的一致性。虽然传统的方法学已经在追求概念的一致性,但是忽略了系统的庞大性,不论系统多庞大,在系统任何位置同一概念通用。DDD 不追求全局的一致性,而是将系统拆成多块,在相同的上下文中实现概念一致性。
白话例子:
以前我们常常说:「公司里说到‘客户’就只能有一种统一定义。」但随着系统越来越大,你会发现销售要的「客户」信息和客服要的「客户」信息完全不是一回事:销售关心成交机会、线索来源;客服关心投诉记录、服务等级。DDD 的做法是承认这种差异:在「销售上下文」里有一个 SalesCustomer 模型,在「客服上下文」里有一个 SupportCustomer 模型,各自在自己的领域里保持定义清晰统一,而不强行做成一个大而全的 Customer 到处用。
识别上下文可以从概念的二义性着手,比如商品的概念在物流、交易、支付含义完全不一样,但具有不同内涵和外延,实际上他们处在不同上下文。
白话例子:
以「商品」为例:
- 在「交易系统」里,商品重点是价格、上下架状态、可售库存;
- 在「物流系统」里,商品重点是体积、重量、包装方式、是否易碎;
- 在「支付系统」里,商品甚至可能只是一个金额和品类代码。
虽然口头都叫「商品」,但你一看业务属性就知道,它们其实属于不同的限界上下文,不能混为一谈。
界限上下文可以用于微服务划分、避免模型的不正确复用带来的问题。
白话例子:
如果你按照限界上下文来划分微服务,可能会有「商品服务」「订单服务」「支付服务」「仓储服务」等,每个服务内部只使用自己上下文内的领域模型。这样就不会出现「仓储系统直接复用交易系统的商品模型,结果发现字段不对又乱加一堆属性」这种越用越乱的问题。
3.4 实体(Entity)
实体(Entity)是在相同限界上下文中具有唯一标识的领域模型,可变,通过标识判断同一性。
白话例子:
在「账户上下文」里,一个银行账户就是一个实体。它有一个唯一账号(比如卡号或内部 ID),这个账号在系统中永远代表同一个账户对象。即使账户的余额、持有人地址等信息变了,只要这个 ID 没变,我们就认为还是「同一个账户」。也就是说,实体的「身份」比它当前的属性值更重要。
3.5 值对象(Value Object)
值对象(Value Object)是一种特殊的领域模型,不可变,通过值判断同一性。
白话例子:
比如一个「金额」:由「币种」和「数值」两个属性组成,100 CNY 和 100 USD 是两个不同的金额,100 CNY 和 101 CNY 也不是同一个金额。如果你想从 100 CNY 变成 101 CNY,做法不是“修改原来的金额对象”,而是“创建一个新的金额对象”。两个金额是否相同,只看这两个属性是不是一模一样。
实体可以使用 ID 标识,但是值对象是用属性标识,任何属性的变化都视为新的值对象。比如一个银行账户,可以由 ID 唯一标识,币种和余额可以被修改但是还是同一个账户;交易单中的金额由币种和数值组成,无论修改哪一个属性,金额都不再是原来的金额。
白话例子:
继续刚才的例子:
- 「银行账户」:有一个账户 ID,不管余额从 1000 改到 500,这个账户仍然是同一个实体。
- 「金额」:一笔交易金额从「100 CNY」改成「200 CNY」,我们并不说是“修改了金额这条记录”,而是说“原来的金额对象不再被使用,新的金额对象取而代之”。在代码中,通常我们会让金额类没有 set 方法,只能新建,保证它是不可变的值对象。
3.6 聚合(Aggregate)
聚合(Aggregate)是一组生命周期强一致,修改规则强关联的实体和值对象的集合,表达统一的业务意义。
白话例子:
以「订单」为例:一个订单包含「订单主信息」(订单号、买家、状态、总金额等)和多个「订单项」(每个商品的数量、单价等),还有可能包含收货地址这种值对象。这一整组东西其实代表了一个完整的「订单业务概念」,它们的创建、修改、删除都应该按统一规则一起考虑,不是各自随意操作。
聚合的意义在于让业务统一、一致,在面向对象中有非常重要价值。比如,订单中有多个订单项,订单的总价是根据订单项目计算而来的。如果没有经验的开发者直接对订单项的做出修改,而不是由订单统一处理业务逻辑,会造成业务的一致性问题。
白话例子:
如果开发者直接在数据库中修改某个订单项的数量,却忘记同时更新订单的总价,就会导致「订单项金额之和 ≠ 订单总金额」这种业务错误。把「订单 + 订单项」作为一个聚合后,系统规定必须通过订单这个入口来修改订单项:你只能调用「订单.添加商品」「订单.修改商品数量」之类的方法,由订单内部统一负责重新计算总价,从而保证数据一致。
聚合需要在相同的上下文中,不能跨上下文。
白话例子:
「订单」聚合不会同时把「库存」实体也拉进来,因为库存属于「仓储上下文」,而订单属于「交易上下文」。二者之间可以有交互(例如下单时锁定库存),但它们各自在自己的聚合里管理各自的一致性,而不是拼成一个跨上下文的大聚合。
3.7 聚合根(Aggregate Root)
聚合根(Aggregate Root)是聚合中最核心的实体,其他的实体和值对象都从属于这个实体。
白话例子:
在「订单聚合」里,订单主实体(Order)就是聚合根;订单项(OrderItem)只是从属实体,收货地址是值对象。外部系统不会直接去改某个订单项,而是都通过订单这个聚合根来操作整个订单相关的东西。
要管理聚合必须使用一个聚合根,然后使用聚合根来实现发现、持久化聚合的作用,完成统一的业务意义。一个聚合中有且只有一个聚合根,聚合也可以只有一个单独的实体。
白话例子:
仓储层通常只会提供「根据订单 ID 读取订单」「保存订单」这样的接口,不会给你「单独保存订单项」的接口。因为「订单」是聚合根,你拿到订单聚合根后,里面自带所有订单项、地址等信息,一起被加载和持久化。对于更简单的场景,一个聚合可能就只有一个实体,比如「用户聚合」只有一个 User 实体本身,那么这个实体既是聚合,也是聚合根。
4 软件设计类概念
4.1 模块(Module)
模块(Module)一组类或者对象组成的集合。
白话例子:
可以把模块想成「一个功能块的代码文件夹」。比如在一个电商系统里,你可能有 user 模块(里面是用户相关的类)、order 模块(订单相关类)、payment 模块(支付相关类)。它们就是一坨彼此相关的类和对象,被打包放在一起,方便管理和理解。
在 DDD 实践中推荐使用限界上下文和聚合来指导模块划分。同时,如果不是特别复杂的业务逻辑也可以不遵循该模式。
白话例子:
如果你发现「订单」这块业务已经复杂到有自己的限界上下文、多个聚合(订单、支付单、发货单等),那就可以以这些上下文和聚合作为模块边界来划分代码结构。如果只是个简单小系统,比如一个内部工具,只要逻辑不重,直接按「功能菜单」随手分几个模块也未必是错的,没必要强行上 DDD 那套复杂划分。
4.2 仓储(Repository)
仓储(Repository)是以持久化领域模型为职责的类。
白话例子:
你可以把仓储理解为「专门和数据库打交道的类」。比如 UserRepository 负责把 User 实体查出来、保存回去,但不管业务校验、不管业务流程,只负责“帮你把对象放进数据库、从数据库拿出来”。
仓储的目的是屏蔽业务逻辑和持久化基础设施的差异。例如,对于同样的持久化业务需求,在采用关系型数据库和非关系型数据库作为存储基础设施时的实现细节是有所不同的。
白话例子:
业务层只会说:「帮我保存这个订单」「根据 ID 把订单查出来」,至于是用 MySQL、MongoDB 还是文件系统,业务层完全不关心。今天你用 MySQL 的实现,明天换成 Elasticsearch,只要仓储接口不变,业务代码就不用改。
软件的设计往往是围绕着对数据的修改完成的。经验不多的开发者往往会认为,软件的开发过程就是对数据库的增删改查。但实际上基于该认知的软件设计让软件难以维护。
白话例子:
很多初级项目全是 UserMapper.updateXXX、OrderMapper.selectYYY 到处飞,业务代码里直接拼 SQL 或各种 Mapper 调用,时间一长你根本分不清「业务逻辑」和「数据访问」混在哪儿,改一个字段要改一大堆地方。用仓储把这些「数据细节」统一装进一层,业务只看“操作对象”,不看“操作表结构”,维护成本就小很多。
对于采用关系数据库作为存储基础设施的项目,仓库层可以被 ORM 实现。若不使用 ORM,则需自己实现仓库。
白话例子:
如果你用的是 Spring Data JPA、MyBatis-Plus 之类的 ORM,它们本身就帮你做了一半的仓储工作:定义个接口,就能增删改查。如果你不用 ORM,比如直接用 JDBC 或 HTTP 调接口,那就要自己写 UserRepositoryImpl 去封装这些细节。
4.3 服务(Service)
服务(Service)是领域模型的操作者,负责领域内的业务规则的实现。
白话例子:
可以理解成「拿着领域对象干活的人」。例如 OrderService 不自己存数据,但会拿到订单实体,检查能不能支付、能不能取消,然后根据规则去变更它的状态、调用仓储保存等等。
领域模型用于承载数据和系统状态,服务承载业务逻辑的实践。
白话例子:
订单实体里保存的是「订单号、金额、状态」这些数据,而「订单在已发货状态下不能取消」「取消要回滚优惠券」等流程和规则,就放在服务里实现。实体更像“数据的形状和基本行为”,服务则是「把一堆数据拿来串成一个完整业务操作」。
在实践中如果使用主、客体的思维来进行设计,则服务为主体,领域模型为客体。使用拟人化的方式来对服务进行命名,可以让开发者更容易理解。比如,一个维护客户数据的 CRM 应用中,客户数据被抽象为模型: Client,对应的服务可以设计为:ClientManager。
白话例子:
你可以把 Client 看作「客户卡片」,而 ClientManager 就是「客户经理」:客户卡片本身只记录姓名、电话、级别;而客户经理(服务)负责「创建客户、合并重复客户、升级客户等级」。用这种拟人化命名方式,读代码很自然就能理解:是谁在操作谁,谁负责做决定。
4.4 工厂(Factory)
工厂(Factory)是以构建领域模型(实体或值对象)为职责的类或方法。
白话例子:
比如你在创建订单时,需要根据用户信息、购物车内容、优惠券、活动折扣等一堆参数计算出最终价格,然后生成一个「状态正确、数据完整」的订单对象。这种「把复杂构建逻辑封在一处」的类或方法,就可以设计成 OrderFactory.createFromCart(...)。
工厂可以利用不同的业务参数构建不同的领域模型。对于简单的业务逻辑实现可以不使用工厂。工厂的实现不一定是类的形式,也可以是具备工厂功能的方法。
白话例子:
简单场景下,你 new 一个对象就够了:
User user = new User(id, name);
但复杂场景,例如根据「注册渠道(App/小程序/线下导入)」来决定用户初始状态、赠送积分、默认标签时,就可以写一个 UserFactory.createByChannel(channel, profile) 来统一封装这套规则。这个工厂既可以是一个单独的类,也可以是一个静态工具方法。
在面向对象程序设计中,工厂是一种设计模式。在广义的工厂模式中,工厂可以根据不同的规则的业务需求构造不同的对象。例如在 Redis 连接客户端的实现中,可以使用 Redis 单机、哨兵、集群等不同的方式来构建 Redis 连接客户端。
白话例子:
比如你用一个 RedisClientFactory 来创建 Redis 客户端:
- 如果配置里写的是「单机模式」,工厂就返回一个普通客户端;
- 如果写的是「哨兵模式」,就返回一个带哨兵的客户端;
- 如果是「集群模式」,就创建一个支持分片和重试的客户端。
业务代码只调用RedisClientFactory.create(config),不需要知道里面到底是 new 的哪个具体实现类。
4.5 策略(Strategy)
策略(Strategy)是业务规则的实现方式。
白话例子:
你可以把策略理解为「同一种事情的不同做法」。比如「计算运费」就是一条业务规则,但可以有「按重量计费」「按体积计费」「按地区包邮」等不同策略。系统根据订单的具体情况,选用对应策略实现来算运费。
例如通知业务,可以使用不同的渠道来实现,不同渠道的实现逻辑可以认为是不同的策略。 在面向对象程序设计中,策略模式也是一种设计模式,是多态的一种实现模式。
白话例子:
发通知的时候,你可以有 EmailNotificationStrategy、SmsNotificationStrategy、WeChatNotificationStrategy 等不同类,都实现同一个接口 NotificationStrategy.send(message, target)。调用方只拿到一个接口引用,至于底层用的是短信还是邮件,由运行时选择合适的策略类来执行。
策略通常会搭配着接口来设计。如果说接口是一种契约,那策略就是契约的履约方式。
白话例子:
例如你先定义好接口:
public interface DiscountPolicy {
Money calculate(Order order);
}
这相当于签了一份「怎么打折」的契约。各种打折方式——满减、打折、会员价——都是这份契约的不同履行方式(不同策略类)。调用方不关心是哪种打折,只要拿到一个 DiscountPolicy 来算就行。
4.6 规格(Specification)
规格(Specification) 是一些特殊的业务规则。通常表现为用于校验(e.g. 数据格式,业务逻辑)、查询和搜索条件。
白话例子:
可以把规格想成「可组合的业务条件对象」。例如「订单必须是已支付状态」「金额大于 100 元」「下单时间在本月内」等,每一条都可以做成一个规格对象,用来做校验或构造查询条件。
在实践中,规格既可以被设计为灵活的查询或校验条件,也可以被抽象出来以便复用。
白话例子:
例如你有一个 ActiveUserSpecification 表示「用户未被封禁且已激活」。这个条件既可以用来:
- 在用户登录时做校验「这个用户能否登录」;
- 在后台查询「当前活跃用户列表」时做过滤条件。
写成一个可复用的规格类,就比在各处复制粘贴 if 条件灵活得多。
例如,在 JPA、MongoDB、ElastiSearch 和一些具有查询能力的 ORM 都大量使用这种设计方式,同样的在应用程序中我们也可以参考这种设计模式,把业务的规则提取出来。
白话例子:
比如 Spring Data JPA 的 Specification<T> 就是典型实现:你可以组合 nameLike(...) && ageGreaterThan(18) 这样的条件对象,最后交给 JPA 去生成 SQL。我们在业务代码里也可以模仿这种方式,把「复杂查询/校验逻辑」抽成 Specification,既利于组合,也利于单元测试。
4.7 分层架构(逻辑)
分层架构是指在软件设计过程中按照既定的原则将不同的功能实现拆分到不同的层级进行实现的一种设计方式。每个层级有独立的职责,多个层次协同以提供完整功能。按照 DDD 的分层模型,通常可以划分为:接入层、应用层、领域层、基础设施层。
白话例子:
可以把它想成「一栋楼的不同楼层」:一层负责对外接待(接入层),二层负责安排具体办什么事(应用层),三层是真正做事的业务部门(领域层),地下室是机房和水电设施(基础设施层)。每层各干各的,协同起来才能对外提供完整服务。
分层架构在具体的软件中可以表现为不同的形式。例如,在分布式系统中,不同层级的软件实现,可以表现为独立部署的服务。而在单体系统中,分层可以用不同的模块或包来实现。
白话例子:
- 在一个单体 Spring Boot 项目里,你可能用
controller、service、domain、infrastructure这些包名来体现分层; - 在微服务架构里,甚至可能是「网关服务」做接入层、「业务服务」做应用+领域、「基础服务」负责基础设施访问。逻辑上是同一套分层思想,物理表现形式可以不同。
分层架构的设计理念与计算机网络的层级结构类似,上层依赖下层的实现,而下层实现无需关心上层实现。例如,HTTP 协议构建在 TCP 协议之上,TCP 协议只负责提供传输层的能力,而不需要知道具体的应用层协议。
白话例子:
就像浏览器发送 HTTP 请求时完全不管「底层怎么分包重传」,这都是 TCP/IP 该操心的。对应到系统里:应用层调用领域层时,不需要在意「数据是存 MySQL 还是 Redis」,这些交给基础设施层即可。
分层架构中层级的数量需要依照系统复杂度来定,并不需要死板地按照 DDD 推荐的四层来进行设计。在简单的系统中,可以通过减少分层来避免样板代码,减少冗余。例如,在 web 系统中有时候只有一种接入方式,接入层和应用层能力高度重叠,可以考虑直接使用应用层代替接入层。
白话例子:
如果你只是写一个内部后台管理系统,全是 HTTP + JSON,没有 App、IoT、消息队列等多种接入方式,那完全可以不单独搞一个「接口层」,直接在 Controller 里承担「接入 + 应用」的双重职责,少一层就少一堆样板代码。
软件框架的使用,通常会引入新的层级,从而影响系统整体的分层架构。例如,ORM 框架本身就提供了对 Repository 的一层抽象。
白话例子:
比如用了 Spring Data JPA,你实际上多了一层「JPA Repository」抽象:它坐在领域层和数据库之间,帮你生成 SQL。再比如使用 API Gateway 框架,相当于在系统前面又加了一层「网关层」,这都会对整体分层形态产生影响。
4.8 接入层(Interface)
接入层负责的是系统的输入和输出。
白话例子:
接入层就像公司的前台或客服:外部的 HTTP 请求、MQ 消息、WebSocket、MQTT 数据,都是先到这里「报到」,再被转交给内部系统处理;处理完的响应,也由这里再发回给外部。
接入层只关心沟通协议,不关心业务相关的数据校验。 接入层的实现是与业务应用强相关的,不同的业务应用有不同的实现方式。例如,对于普通的 Web 应用,基于 HTTP 协议的 API 是一种接入层实现方式;对于 IoT 传感器的数据上传业务,接入层的实现可能需要基于 websocket 或 MQTT 协议。
白话例子:
比如在 Web 项目中,Controller 就是接入层的一部分:它只负责「解析 HTTP 请求参数」「返回 HTTP 响应」,不负责判断“订单能不能取消”。而 IoT 平台里,负责接收设备通过 MQTT 上传数据的那一层,也只是负责解析消息格式,把解析好的数据往下游丢。
接入层的特点:
- 接入层对应用数据透明,只关心数据格式而不关心数据的内容
- 在大部分单体系统中接入层通常被框架实现。例如,在 Spring Boot 框架中,HTTP 协议的 API 设计不需要关注 HTTP 协议本身。
- 在分布式系统中接入层通常被网关实现。
白话例子:
- 对接入层来说,「这个字段叫
userId」和「这个字段叫uid」只是两种格式,它不管userId在业务上代表什么含义; - 用 Spring Boot 时,你只写
@RestController,很少关心 TCP 套接字和 HTTP 报文,是框架帮你扛了大部分接入层工作; - 在微服务里,一个 API Gateway(如 Kong、Nginx+Lua、Spring Cloud Gateway)负责接所有外部请求,再路由到内部服务,本身就是一个专门的接入层。
4.9 应用层(Application)
应用层,组织业务场景,编排业务,隔离场景对领域层的差异。
白话例子:
可以把应用层看成「流程编排员」。它拿到请求后,决定:先查用户,再校验权限,再调用领域服务创建订单,再发消息给通知系统……这些步骤怎么排、哪些步骤要不要做,都在应用层里组织。
应用层遵循面向对象核心思想中的 “关注点分离” 理念。应用层的关注点在于业务场景的处理。例如,对于一个服务多种类型用户的应用,to C 的网页界面和后台管理界面对应的是不同的业务场景。对于新用户注册这个业务来说,通过 to C 的网页注册和通过后台管理界面进行后台注册是不同的业务场景。然而,“用户注册”在系统层面的基本逻辑是一样的。所以,“用户注册”的基本业务逻辑可以交由领域层来实现。而两种不同渠道进行用户注册所需要进行的身份验证等逻辑,可以设计在应用层进行实现。这样便能达到关注点分离,复用核心业务逻辑的目的。
白话例子:
对用户来说:
- C 端注册场景:要短信验证码、要图形验证码,注册完要送优惠券;
- 后台运营帮用户录入:只允许内部员工操作,要校验员工权限,可能不送券。
但是无论哪种入口,本质都是「创建一个用户账号」。所以: - 创建用户账号的核心规则放到领域层(比如必须要唯一邮箱、要加密密码);
- 具体是谁注册、从哪来、送不送券,这些每个场景不同的流程逻辑,就写在不同的应用服务里。
应用层的特点:
- 关心处理完一个完整的业务
- 该层只负责业务编排,对象转换,而具体的业务逻辑由领域层实现
- 虽不关心请求从何处来,但关心谁来、做什么、有没有权限做
- 利用不同的领域服务来解决问题
- 对最终一致性有要求的业务和事务处理需要放到应用层来处理
- 功能权限放到这层
白话例子:
比如一个「提交订单」的应用服务:
- 它会检查「当前用户是否登录、有无下单权限」;
- 然后调用「购物车领域服务」「订单领域服务」「库存领域服务」等;
- 最后可能发一个「订单创建成功」的事件给消息队列以做后续处理(如积分结算、短信通知),这类最终一致性事务编排也在应用层完成。
但像「订单金额怎么算」「库存锁定规则是什么」,仍然在各自的领域服务里实现。
4.10 领域层(Domain)
领域层,实现具体的业务逻辑、规则,为应用层提供无差别的服务能力。
白话例子:
领域层就是真正「懂业务」的那一层。比如「订单有效状态有哪些」「优惠券使用条件如何判定」「库存扣减的规则是什么」,这些让业务专家点头的规则,都应该体现在领域模型和领域服务里,而不是散落在 Controller 或 DAO 中。
实际处理业务的地方,领域层需要对应用层提供无差别的服务和能力。例如,对于用户注册的场景,用户既可以通过邮箱自己注册,也可以由管理员在后台进行添加。用户注册的核心逻辑可以由领域层完成,但是对于不同渠道进行用户注册的参数校验和权限验证等逻辑则由应用层实现。
白话例子:
不管是「自己注册」还是「管理员代注册」,只要最终都要得到一个「合法的 User」,那所有「创建 User 必须遵守的规则」(比如手机号必填、密码长度不少于 8 位、用户名唯一)就统一放在领域层来校验和构建。渠道不同只是「入口方式不同」,而「核心怎么创建」这件事对所有渠道来说是一样的。
领域层的特点:
- 不关心场景,关心模型完整性和业务规则
- 不关心谁来,不关心场景完整的业务,关心当前上下文的业务完整
- 强一致性事务放到这层,聚合的事务是 “理所当然的”
- 对应到分布式系统中的 domain service、后台等概念
- 领域层做业务规则验证
- 数据权限放到这层(比如只允许删除自己创建的商品),因为数据权限涉及业务规则
- 根据业务情况,参考反范式理论,跨上下文使用值对象做必要的数据冗余
白话例子:
- 领域层不会管「这个请求来自小程序还是管理后台」,但会管「这个用户有没有权限删这条记录」;
- 像「修改订单」这种必须强一致的操作,领域层会在一个事务里完成对整个聚合的更新;
- 数据权限也是业务规则的一部分,比如「只有创建人才能删除商品」,就应在领域服务里判断,而不是只在接口层拦一拦;
- 有时候为了减少跨服务查询,你会在订单里冗余一份「下单时的商品名称和价格」这类值对象,这是有意识的领域设计,而不是数据库层面的“脏冗余”。
4.11 基础设施层(Infrastructure)
基础设施层,提供具体的技术实现,比如存储,基础设施对业务保持透明。
白话例子: 可以把基础设施层理解成「水电煤」。做饭这件事是「业务」,而自来水公司、电力公司就是「基础设施」。做饭(业务逻辑)不用关心电是怎么发的、水是怎么净化的,只要能正常用就行。同样,业务代码只关心「要存一条订单」,不需要关心数据库连接细节、驱动如何配置等,这些都由基础设施层来处理。
对于基础设施层来说,基础设施层并不是指 MySQL、Redis 等外部组件,而是外部组件的适配器,Hibernate、Mybatis、Redis Template 等,因此在 DDD 中适配器模式被多次提到,基础设施层往往不能单独存在,还是要依附于领域层。技术设施层的适配器还包括了外部系统的适配,互联网产品系统的外部系统非常多,常见的有活体监测、风控系统、税务发票等。
白话例子:
这里强调:基础设施层不是 MySQL 本身,而是“会用 MySQL 的那层代码”。
比如:
- 你并不把「MySQL 数据库」本身当作基础设施层,而是写一段用 MyBatis 操作 MySQL 的代码,这段代码才属于基础设施层;
- 当你接入一个「风控系统 API」,你会写一个
RiskServiceClient去把复杂的 HTTP 调用封装起来,对上层暴露一个简单方法checkUserRisk(userId),这个RiskServiceClient就是基础设施层的适配器; - 领域层里只会说「我要查询订单」「我要做风控校验」,不会看到各种 SQL、HTTP 细节,因此基础设施层是粘在领域层下面、专门帮领域层“接线”的那一层。
技术设施层的特点:
关心存储、通知、第三方系统等外部设施(防腐层隔离)
基础设施的权限由配置到应用的凭证控制,例如数据库、对象存储的凭证,技术设施层不涉及用户的权限
【白话案例】
- 「关心存储、通知、第三方系统」:
比如你要:- 往数据库写订单(存储);
- 发短信、发邮件(通知);
- 调用支付平台、税务系统(第三方)。
这些跟“业务规则”没太大关系,但跟“外部世界怎么打交道”强相关,都属于技术设施层。它们也是常说的「防腐层」,用来隔离外部的格式、协议、奇怪规则,不让这些细节污染到领域模型里。
- 「由配置的凭证控制,而不是用户权限」:
例如:- 连接数据库要用数据库账号密码(写在配置里);
- 调用对象存储要用
accessKey/secretKey;
这些是“应用与外部系统之间”的权限,而不是“张三能不能看这个订单”的权限。后者属于业务权限控制,不是基础设施层管的。
4.12 部署架构(物理)
部署架构是指具体的架构实现
白话例子: 逻辑上你可以说「我有用户服务、订单服务、支付服务」,但在现实中,这些服务是怎么部署、跑在哪几台机器上,就属于部署架构的问题:
- 是所有东西都打成一个包放在一台服务器上?
- 还是拆成多个进程,分别部署在多台机器?
- 有没有负载均衡、网关、缓存服务器?
这些“物理上怎么放、怎么连”的整体布局,就是部署架构。
主要是在分布式系统、单体系统,甚至在客户端软件中体现。
【白话案例】
- 对一个单体系统来说,部署架构可能就是「一台服务器 + 一个应用 + 一个数据库」。
- 对一个分布式系统 / 微服务系统来说,部署架构可能是「几十个服务,每个服务若干实例,分布在多台机器或多个机房」。
- 甚至在客户端软件里,你也会考虑「哪些逻辑跑在本地,哪些调用云端服务」。
这些都是“在真实世界里,代码到底怎么分布、怎么部署”的体现。
把逻辑架构和部署架构区分开可以很好的理解软件设计上和部署上的不同,对于应用构架来说,逻辑上的设计不一定对应部署结构。
白话例子:
你可以在设计时画出很漂亮的分层架构图:接口层、应用层、领域层、基础设施层……这叫逻辑架构。
但真正部署时,可能是:
- 所有分层都打成一个
jar/war放在一台机上(单体部署); - 或者一个大系统里的不同领域被拆成几个独立服务,各自有自己的分层(微服务)。
逻辑上的“有几层、怎么调用”不等于物理上“有几个进程、几台机器”。分清这两件事,能避免很多误解。
这样就很好理解 DDD 在不同场合中的使用方式,避免生搬硬套。当 DDD 的分层结构在单体应用中使用时,每层可能使用包、模块来表达,在微服务中使用时候,每层可能由不同角色的微服务来完成。
【白话案例】
- 在单体应用里,你通常是用「包 / 模块」来表示 DDD 的各个分层,比如:
com.xxx.user.interfacecom.xxx.user.applicationcom.xxx.user.domaincom.xxx.user.infrastructure
它们最后一起打包部署成一个应用。
- 在微服务里,有的团队会这么做:
- 用户侧下单相关逻辑做成一个“下单服务”;
- 商户管理、定价等做成另一个“商户服务”;
这些服务内部再各自采用 DDD 分层。
也就是说:同样一套 DDD 思路,在单体中用“包”体现,在微服务中可能表现为“多个服务 + 各自内部再分层”。不用把书上的例子机械照抄到任何场景。
4.13 微服务(Micro Service)
微服务是一种低耦合的分布式应用系统。
白话例子: 想象一个电商系统,被拆成很多小系统:用户服务、商品服务、订单服务、库存服务、支付服务……
- 每个服务可以独立开发、部署和扩容;
- 它们通过网络(HTTP、RPC 等)相互调用完成整体业务;
- 改订单服务时,不一定需要重启商品服务。
这种“拆成一堆小块、通过网络协作”的风格,就是微服务。
维基百科的定义是:一种软件开发技术 - 面向服务的体系结构(SOA)架构样式的一种变体,将应用程序构造为一组松散耦合的服务。这个定义没有问题,但是忽略了一个重要的信息,微服务是一种分布式架构,微服务必须面对分布式系统的各种问题。
白话例子: 很多人只记得“微服务 = 把系统拆成很多小服务”,但忽略了:
- 这些小服务是通过网络互相调用的,所以:
- 网络会抖动、会超时、会半截失败;
- 数据在多个节点之间可能不一致;
- 调用链会很长,排错变难。
因此,做微服务不是“画几条服务边界线”那么简单,而是要真正在分布式环境下活下去,面对各种网络、数据一致性、故障恢复的问题。
分布式系统是通过计算机网络连接、协同工作的 IT 系统,因此在使用 DDD 时候,需要为这种系统做适配,而不是简单的做出切分。
白话例子: 如果你直接把一个单体的领域模型照抄一份,简单按领域边界“硬拆”成多个服务,很快会遇到:
- 事务跨服务怎么做?
- 一个操作要调用 3 个服务,失败时如何回滚?
- 读数据时要聚合多个服务的数据,怎么保证性能和一致性?
所以,在分布式环境下使用 DDD,要考虑: - 哪些聚合放一起,减少跨服务调用;
- 通过领域事件、最终一致性等方式适配分布式的特性;
而不是只做“纸面上的切分”。
4.14 单体(Monomer)
单体是主要业务实现和部署在单一服务器上的应用。
白话例子: 常见的“传统项目”就是单体:
- 一个大
war包丢到一台 Tomcat 上; - 全部业务代码都在同一个进程里;
- 数据库可能也是一台。
你改了任意一行业务代码,都要重新打包、重新发布整个应用。这就是典型的单体。
单体系统是相对于微服务来说的,其特点是主要的实现在单一的服务器中。
白话例子: 可以简单理解为:
- 微服务:很多“小服务 + 多台机器 + 网络通讯”;
- 单体:一个“大应用 + 通常一台主服务器”。
比如一个中小企业的内部管理系统,部署在公司机房的一台服务器上。所有功能(人事、财务、考勤、报销)都在一个大项目里开发、打包、发版,就属于单体系统。
4.15 分布式应用系统(Distributed)
分布式应用系统是建立在计算机网络之上的应用软件系统,不同单元通过计算机网络集成。
白话例子: 只要你的系统满足:
- 有多个节点 / 进程 / 服务;
- 它们需要通过网络相互调用;
- 整体才能完成业务。
那就是分布式应用系统。
比如: - 多个微服务 + 消息队列 + Redis + 多个数据库节点;
- 或者一个应用拆成前端网关层、业务服务层、数据处理层,跑在不同机器上,通过网络协作。
5 事件风暴类概念
5.1 事件风暴(Event Storming)
事件风暴是一种以工作坊的形式,使用 DDD 建模的方式。
白话例子: 事件风暴有点像一次「高强度的业务 + 技术头脑风暴会」,所有人围在一块白板或墙前,拿着便利贴:
- 写下“发生了什么事”(事件);
- 贴在墙上按时间线、关联关系排布;
- 一边讨论一边调整。
它不是坐在工位上写文档,而是用「贴纸 + 走来走去 + 现场讨论」的方式做领域建模。
事件风暴的发明人是 Alberto Brandolini ,它来源于 Gamestorming,通过工作坊的方式将领域专家和技术专家拉到一起,进行建模。
白话例子: 在实际项目中,你会看到:
- 产品经理、业务负责人(领域专家)站在一边,讲业务;
- 架构师、开发(技术专家)站在另一边,问各种细节;
- 大家一起往墙上贴不同颜色的便签:事件、命令、角色、外部系统……
这种「大家在一间房里一起画、一起贴」的活动,就是事件风暴工作坊。
事件风暴是一种捕获行为需求的方法,类似传统软件的开发用例分析。所有人员(领域专家和技术专家) 对业务行为进行一次发散,并最终收敛达到业务的统一。
白话例子:
传统做法是:写用例文档、流程图,然后来回评审。
事件风暴的做法是:
- 先让大家把所有可能的业务行为都「倒」出来(大面积发散:各种事件、各种场景);
- 再一起整理、分组、合并(收敛成一套大家都认同的业务流程和规则)。
结果是:业务方觉得「这就是我们干的事」,技术方觉得「我知道系统应该怎么设计」。双方对“系统到底要做什么”达成统一认知。
5.2 领域事件(Domain Event)
事件是系统状态发生的某种客观现象,领域事件是和领域有关的事件。
白话例子: “事件”可以理解为「系统里某件有意义的事情发生了」。
- 比如:订单已创建、支付已完成、发货已出库、发票已开具。
只要这个“事情”是跟我们关心的业务领域直接相关的,就可以叫做“领域事件”。
领域事件(Domain Event),是在业务上真实发生的客观事实,这些事实对系统会产生关键影响,是观察业务系统变化的关键点。领域事件一般是领域专家关心的。
【白话案例】
- 「用户完成支付」是真实发生的事实,系统要:
- 记录支付结果;
- 触发发货流程;
- 生成积分或优惠券。
- 「合同已签署」会影响:客户状态、后续服务流转、数据统计等等。
对领域专家来说,问一句:「你们业务里有哪些关键节点,一发生就会引起一连串动作?」通常得到的答案就是领域事件。
事件的评价方式是系统状态是否发生变化。系统状态变化意味着领域模型被业务规则操作,这是观察系统业务的好方法。
白话例子: 判断“这是不是一个事件”的一个简单标准:
- 发生这件事前后,系统里有没有什么「状态」变了?
- 订单从「待支付」变成「已支付」;
- 用户从「未实名」变成「已实名」。
如果只是“看了一眼页面”但系统里什么都没变,就很难算是一个关键领域事件。
识别领域事件的线索有:
是否产生了某种数据
系统状态是否发生变化,无论这种状态存放到数据库还是内存
是否对外发送了某些消息
白话例子: 在事件风暴里,你可以用这三条来「抓事件」:
- 是否产生了新数据?
- 生成了订单号、支付流水号、合同编号等。
- 系统状态是否变化?
- 用户的等级从「普通」变成「黄金」;
- 工单从「处理中」变成「已关闭」。
- 是否通知了别人?
- 给用户发了短信、推送;
- 给其他系统发了消息(如 MQ、Webhook)。
每当你发现这些迹象,通常可以考虑是不是应该定义一个领域事件。
5.3 业务规则(Policy)
业务规则是指对业务逻辑的定义和约束。
白话例子: 业务规则就是「公司是怎么做事的明确规定」,例如:
- 下单金额超过 $$1000$$ 元必须先审批;
- 一个手机号一天最多发送 $$5$$ 条验证码;
- 订单在未支付状态下超过 $$30$$ 分钟自动取消。
这些跟“技术细节”无关,而是对“业务行为”做约束的条款。
不同的业务规则往往意味着不同的领域事件被触发,未来在技术实现时可能是一些分支条件,对应 DDD 实现中可能通过领域服务、规格、策略等方式实现。
白话例子: 比如下单后:
- 如果金额小于 $$1000$$,事件可能是「订单已自动通过审核」;
- 如果金额大于等于 $$1000$$,事件可能是「订单已进入人工审核」。
这两种不同的后果,其实就是不同的业务规则在起作用。实现上可能是: - 代码里一个
if分支; - 或者一个策略类
HighAmountOrderPolicy、NormalOrderPolicy。
但在领域层面,这是规则在驱动后续领域事件的差异。
业务规则的识别是为了将数据和算法分开。
白话例子:
很多系统一开始会把「数据结构」和「各种 if/else 逻辑」混在一起,越写越乱。
把业务规则单独识别出来,你可以做到:
- 数据:比如“订单有哪些字段”;
- 算法 / 规则:比如“在什么条件下订单算超时、能否退款”。
这样更容易: - 单独修改规则而不动数据结构;
- 单独测试规则是否正确;
- 给领域专家解释「规则是怎么写的」。
5.4 命令(Command)
命令是执行者发起的操作,构成要件是执行者和行为。
白话例子: 命令可以理解为:「谁,要让系统干什么」。
- 用户点击「提交订单」按钮,这是一个命令;
- 外部系统调用我们的
POST /invoices接口让我们开具发票,这是一个命令。
只要能说清楚「执行者 + 动作」,通常就是一个命令。
命令可以类比于 UML 分析中的业务用例,是某个场景中领域事件的触发动作。
白话例子: 在「用户下单」这个场景中:
- 命令:
提交订单(由用户发起); - 触发的领域事件可能有:
订单已创建库存已预占优惠券已扣减
也就是说:命令是“扣扳机”的动作,领域事件是“扣扳机以后发生了什么”。
5.5 执行者(Actor)
执行者是指使用系统的主体,是导致系统状态变化的触发源。
白话例子: 只要某个「主体」能对系统说“你去干点啥”,并让系统真的发生了变化,那它就是执行者:
- 人(用户);
- 另一个系统(外部系统);
- 系统自己(内部定时任务、事件驱动的后续处理)。
执行者有点像 UML 的涉众,不过区别是执行者不仅是用户,还包括外部系统和本系统。 在事件风暴中,执行者可以是:用户、外部系统、本系统、定时器。
白话例子: 在事件风暴的便签墙上,你会看到多种执行者:
- 用户:比如「买家」「卖家」「管理员」;
- 外部系统:比如「支付网关」「税务系统」;
- 本系统:例如「订单服务自动生成对账单」;
- 定时器:每天凌晨 $$3$$ 点自动结算的任务。
这些都是命令的发起者,也是系统状态变化的起点。
5.6 用户(User)
用户是执行者的一种,是指使用软件或服务的人。
白话例子:
用户就是最直观的那类人:打开网页 / App,点击按钮、填写表单的人。
例如:
- 淘宝里的买家、卖家;
- 后台系统里的运营、客服;
只要是“人在和系统交互”,就是用户。
用户可以有不同的角色,通常我们会把不同角色的相似行为作为不同的命令来处理,有可能得到同样的事件。比如系统出现了商品已添加的事件,有可能有多个触发的场景: 1. 系统管理员在后台中添加 2. 商户在自己的管理平台中添加 3. 导入任务在特定时间添加
1 2 是用户的行为,不过是不同的角色。
白话例子: 以「商品已添加」这个领域事件为例,背后可能有:
- 「后台管理员添加商品」命令(角色:平台运营);
- 「商户自行上架商品」命令(角色:商户);
- 「系统定时导入商品」命令(执行者是定时器 / 系统)。
从系统角度看,最后都产生了同一个事件:商品已添加,但触发命令的执行者和上下文是不一样的,所以命令应该区分开来,而事件可以复用。
5.7 外部系统(Out System)
外部系统是执行者的一种,系统开放 API 的调用发起者。
白话例子: 可以把外部系统想成「通过 API 来遥控你的系统的其他系统」。
- 比如:你的电商系统要调用支付宝的支付 API,那支付宝就是你的外部系统;
- 你的 App 要调用微信的登录 API,那微信就是你的外部系统。
它们通过你暴露的接口,让你帮它们做事。
有一些系统会提供对外的 API 给外部系统,这时候外部系统也会发出命令让系统产生事件,这里的外部系统特指作为执行者的外部系统。
【白话案例】
- 你的系统暴露了一个 API:
POST /order,允许外部系统创建订单; - 那么,当一个 ERP 系统调用这个 API 时,它就成为了一个“执行者”,触发了“创建订单”这个命令,导致了“订单已创建”等领域事件。
这里强调的是:这个外部系统通过调用你的 API,成为了你系统状态变化的“推手”。
5.8 本系统(System)
本系统是执行者的一种,指系统本身。
白话例子: 可以理解为“系统自己没事也会做点事”。
- 比如:每天凌晨 3 点自动跑批处理;
- 订单支付超时后自动取消;
- 用户注册 7 天后自动发优惠券。
这些不是用户或外部系统“推”的,而是系统自己“动”的。
事件的触发可以由用户、外部系统、定时器触发,也可以由上一个事件触发,因此这里的触发者(主体)就是系统本身。
【白话案例】
- 用户支付成功后,系统自动发送短信通知;
- 订单发货后,系统自动更新物流状态;
- 每天定时统计报表。
这些事件的触发,不是因为用户点了什么按钮,而是因为“之前发生了什么”,然后系统自己“接力”触发了后续的事件。
5.9 定时器(Timer)
定时器是执行者的一种,通常是定时任务。
白话例子: 定时器就像一个“闹钟”,时间一到就“叮”一下,然后系统开始执行一些任务。
- 比如:每天凌晨 3 点跑数据统计;
- 每月 1 号生成上个月的账单;
- 每隔 5 分钟检查一次数据库连接池是否健康。
这些任务不需要人去点,也不需要外部系统来调,而是“到点自动触发”。
定时器可以作为执行者,不过需要区别于本系统这个触发源。定时器可以看待为外部一个时间信号源,类似于计算机中主机中的振荡器。
【白话案例】
- “本系统”是指“因为发生了 XXX,所以我要做 YYY”;
- “定时器”是指“不管之前发生了什么,到时间我就要触发一个事件”。
前者是“事件驱动”,后者是“时间驱动”。
5.10 参与人(Participants)
作为工作坊的参与人员(应区别于执行者)。
白话例子:
参与人就是“坐在会议室里一起做事件风暴的人”。
他们不是“操作系统的用户”,而是“一起设计系统的人”。
参与人只是一种角色,而非具体的一个人,可以是多个自然人做群体参与,也可以一人分饰不同的角色。
【白话案例】
- 你可以同时扮演“产品经理”和“用户代表”;
- 也可以让 3 个开发一起参与,代表“技术团队”。
重要的是:参与人代表的是一种“角色”和“视角”,而不是非得是谁。
在开始工作坊之前,参与人需要满足一些条件:
- 参与人需要对解决的问题和产出目标达成共识
- 参与人需要 DDD 的基本知识或接受过基本培训
- 领域专家、技术专家需要能全程参加
白话例子: 就像开会前要:
- 明确会议要解决什么问题;
- 大家最好都了解一些 DDD 的基本概念,不然容易鸡同鸭讲;
- 关键人物(领域专家、技术负责人)最好全程参与,不然容易出现“这个人说了不算”的情况。
5.11 领域专家(Domain Expert)
领域专家是指熟悉业务规则的人,在工作坊中一般是能敲定业务规则的人。
白话例子: 领域专家就是“公司里最懂业务的人”。
- 比如:银行的风控专家、电商的运营总监、物流的调度经理。
他们知道“这个业务应该怎么做才对”,能在事件风暴里拍板定规则。
在实际的事件风暴工作坊中,领域专家是一个比技术专家更难获得人,一个合格的、能让工作坊进展下去的领域专家需要有几个要求:
- 了解现有业务个情况
- 能对具体的业务方向做出结论性的输出
- 在做工作坊时,需要分清现状(As-IS)和目标(To-Be)业务,现状业务很多人都能说出来,不过真正的领域专家是能对目标业务做出描述的人。
白话例子: 一个好的领域专家应该:
- 知道现在业务是怎么跑的;
- 能告诉你未来想怎么跑;
- 能在讨论时给出明确的结论,而不是“我觉得”“大概是”。
否则,事件风暴很容易变成“大家一起瞎聊天”。
5.12 技术专家(Tech Expert)
技术专家是指熟悉技术方案和实现方式的人,能给出可行的技术方案和了解基础设计的限制条件。
白话例子: 技术专家就是“公司里最懂技术的人”。
- 比如:架构师、资深开发、技术负责人。
他们知道“这个系统能怎么实现”,能在事件风暴里告诉你哪些技术方案可行,哪些有坑。
技术专家需要能对现有的技术做出描述,而未来的技术选型可能是动态的,能有一定预见性最好。技术专家往往是当前团队中最熟悉架构和代码的人。
白话例子: 一个好的技术专家应该:
- 知道现在系统是怎么搭起来的;
- 能告诉你未来可以用什么新技术;
- 能在讨论时告诉你“这个方案现在做不了”“这个方案以后可能有问题”。
否则,事件风暴很容易变成“大家一起画饼”。
5.13 主持人(Facilitator)
主持人工作坊流程的推动者,以及 DDD 方法论的守护者。
白话例子: 主持人就像“会议组织者 + 规则裁判”。
- 他要保证事件风暴的流程顺利进行;
- 也要保证大家在用 DDD 的正确姿势思考问题。
在一些工作坊中,主持人往往是外部的咨询师,他们有大量的实践经验,需要能对 DDD 的概念、方法有成体系的研究,并能推动工作坊进行。
白话例子: 一个好的主持人应该:
- 熟悉 DDD 的各种概念和方法;
- 有丰富的事件风暴实践经验;
- 能在现场引导大家思考、提问、总结,保证工作坊不跑偏。
否则,事件风暴很容易变成“大家一起随便贴贴纸”。
见:DDD 概念参考