
最近几年,DDD(Domain Driven Design,领域驱动设计)被越来越多的人提起。关于 DDD 的一些概念知识,相信许多人都有所了解。但是在了解 DDD 的概念以后,如何应用到实际项目中,特别是如何从业务需求转换成可以开始编码的对象模型,网上对这个过程的探讨并不多。本文试图运用事件风暴,从一个业务需求实例出发,抽象出 DDD 的战略和战术模型。
下图来自 Eric Evans 的《领域驱动设计:软件核心复杂性应对之道》。该图的下半部分是 DDD 的 Strategic View,现在有种译法叫做 DDD 的战略建模;上半部分是 DDD 的 Building Blocks,译作 DDD 的战术建模。

把 DDD 分成战略建模和战术建模两个阶段的好处是:在系统分析前期可以通过 DDD 战略建模来探索系统边界,识别限界上下文(Bounded Context)和按限界上下文划分子域 (Sub-Domain),把子域进一步分成核心子域、支撑子域和通用子域,然后根据各个子域内部的需求和实际情况来决定下一步是否应用 DDD 战术建模。简单的说,就是可以根据项目实际情况对 DDD 进行裁剪使用。
对系统进行 DDD 战略建模以后,得到的领域模型主要是限界上下文和上下文映射图 (Context Mapping)。对于限界上下文和子域的对应关系,Eric Evans 并没有给出明确的定义,一对一(一个限界上下文对应一个子域)、一对多(一个限界上下文对应多个子域)和多对多都有人提及,也有人认为无需识别子域(或者识别为一对一关系),可以直接基于限界上下文进行进一步分析(例如 DDD 战术建模)和开发。笔者认为从限界上下文进一步识别子域和对子域进行分类,可以明确系统的核心业务域,帮助识别 Back Log 的优先级,对复杂系统的开发很有帮助。
对单个限界上下文进行 DDD 战术建模以后,得到的领域模型主要是:
聚合(Aggregate)
实体(Entity)
值对象(Value Object)
资源库(Repository)
领域服务(Domain Services)
领域事件(Domain Events)
模块(Modules)
开发人员可以在 DDD 战术领域模型的基础上直接开始编码。
在 Alberto Brandolini 提出用 Event Storming 帮助 DDD 建模之前,DDD 的战术和战略建模更多的依赖领域专家和技术人员的设计经验,耗时耗力。Alberto Brandolini 认为其根源是对于复杂系统,存在如下图所示的信息孤岛,导致在项目前期不容易识别出所有限界上下文,到了项目后期需要花费更大代价来纠正前期的遗漏。

事件风暴是以面对面的研讨会(workshop, 又译作工作坊)方式,把系统涉及的领域专家和技术人员聚集在一个房间内,在很短的时间内(一到两天)进行激烈的讨论(头脑风暴),把识别出的领域对象用彩色的即时贴记录下来,按时间发生先后顺序从左到右贴在会议室墙上。事件风暴试图把可能耗时几周的前期设计压缩到几天内完成。下图是一个事件风暴的实际例子:

Alberto Brandolini 的事件风暴分为两个阶段:全景事件风暴(Big Picture Exploration)和设计级事件风暴(Design-level Event Storming),分别对应 DDD 战略建模和战术建模。
在事件风暴中需要注意使用统一语言(Ubiquitous Language)来描述领域对象。例如,在支付的限界上下文内,使用 amount(金额)、payment(名词支付)、credit(信用额度)、deduct(扣款)的文字能统一领域专家和技术人员对业务的定义。因为领域模型最终会落地为源代码,一般建议用英语作为统一语言的文字描述。用名词 + 动词的过去分词表示领域事件,例如:Bill Payed,Credit Deducted。用动词 + 名词表示命令,例如:Pay Bill,Deduct Credit。
因为 Event Storming 对 DDD 建模的影响很大,有人甚至把使用 Event Storming 进行 DDD 建模称为 EDD(Event-Storming Driven Design)。
本文不赘述事件风暴的详细组织方法与会议流程,笔者谨以一个技术人员的视角,以一个酒店客房预订系统为例,描述如何运用事件风暴来对业务系统进行战略和战术建模。
示例系统:某酒店客房预订系统
业务需求:
顾客在酒店网站上输入计划入住日期、房间规格(单人房、双人房、家庭套房等)查询酒店房型,网站返回符合条件的房型列表。
顾客点击房型列表里某房型的链接,可以查看房型详细信息。房型详细信息包括房间规格、床的规格和数量、是否有窗、是否含早餐、是否有上网服务、是否无烟、退预订是否免费等。
顾客可以在房型详细信息页面修改入住日期,网站显示该房型的价格(价格可随入住日期变化)和当天是否有空余房间。
房型详细信息的页面有“加入购物车”链接,顾客可以输入入住日期、入住天数和所需房间数量,并加入购物车;
顾客点击购物车链接,可以浏览和修改已加入购物车的房型和房间数量;
顾客点击预订,录入各房间入住人员信息和订单联系人信息;
顾客点击下一步,网站显示顾客需支付的预订费用和提示顾客可取消预订的截至时间;
顾客点击支付,网站跳转到第三方支付系统的支付页面;
顾客支付预订费用以后,系统发送预订成功的邮件和短信通知。
实战全景事件风暴(Big Picture Exploration):

识别系统关键业务事件
笔者理解的关键业务事件之一,是可以导致业务场景切换的事件。以某酒店客房预订系统为例,容易想到的关键业务事件包括:Booking Order Payed(预订订单已支付),Confirmation Notification Sent(预订确认通知已发送)等。Booking Order Payed 事件意味着离开支付场景,Confirmation Notification Sent 事件意味着离开通知场景。
以下是笔者对某酒店客房预订系统识别出的关键业务事件,按事件产生时间顺序从左到右排列,用橙色即时贴表示:


识别系统其他业务事件
从识别出的关键业务事件,从时间轴向后探索该事件的后置事件,向前探索该事件的的前置事件,逐步识别出系统其他业务事件。
以下是笔者对关键事件 Booking Order Created 识别出的前后置事件,按事件产生时间顺序从左到右排列:


按业务边界划分识别候选限界上下文
对识别出的系统事件进行纵向和横向的切分,识别出候选限界上下文。
纵向切分是针对不同时间段进行切分。在时间轴上看看当前事件、前置事件与后置事件之间的因果关系,然后针对前后因果关系较弱的事件,进行纵向的切分,如此就可以划分出多个候选限界上下文。
横向切分则根据语义相关性进行切分。梳理事件风暴识别出来的所有事件,并确定事件的命名很好地遵循了统一语言。然后针对事件的名称,看是否存在语义上相关的事件。包括名称相同,名称含义相同或相近,都属于语义相关性。将这些语义相关的事件归为一类,并为其命名,即可得到候选的限界上下文。
在识别事件流时,如果已经分为多个完全不同的事件流,则每个独立的事件流必然分属于不同的限界上下文。
以下是笔者识别出的部分候选限界上下文,用蓝色即时贴表示:


按系统边界和架构边界进一步划分或合并限界上下文
系统边界又称为进程边界。因为进程内通信的代价远远小于进程间通信,出于减少开发成本和性能优化的目的,对于关联度较高的限界上下文,可以按系统边界合并为一个限界上下文。有一些系统需要遵循预定义好的特定技术架构,可以针对预定的技术架构划分或合并限界上下文

按其他因素进一步划分或合并限界上下文
其他因素也会影响限界上下文的划分,例如开发团队的组织架构,即康威定律。
以下是笔者识别出的最终的限界上下文,其中 Room Context 合并了上图的 Room Type Context 和 Room Reservation Context,Order Context 合并了 Shopping Cart Context 和 Order Context:

没有做进一步的合并,是希望 Order Context 能被其他业务重用。如果限界上下文和子域作一对一映射,那 Room 子域显然是系统的核心子域,Order Context 可以是核心子域或支撑子域,Payment Context 和 Notification Context 可以是支撑子域或通用子域。

对限界上下文识别上下文映射图
识别限界上下文直接的关联,以及关联的上下游关系,得到上下文映射图。
以下是笔者识别出的上下文映射图:

实战设计级事件风暴(Design-level Event Storming):
通过设计级事件风暴,可以深入探索某个限界上下文的细节,识别出明确的战术级领域模型,参加设计级事件风暴的开发人员应该能够立即开始编码。
相比全景事件风暴,设计级事件风暴要更加模式化。下图来自 Alberto Brandolini 的《Introducing Event Storming》一书:

每一个领域事件,对应一个触发该事件的命令(Command)。例如领域事件 Booking Order Payed,对应的命令是 Pay Booking Order。执行命令的可以是限界上下文内部的一个聚合(Aggregate),或者一个外部系统(External System)。
外部系统可以是系统外部的系统,也可以是系统内部的其他限界上下文。例如,如果系统内部实现了支付功能,那么执行 Pay Booking Order 命令的外部系统可以是支付上下文;如果支付功能是第三方软件提供的,那执行 Pay Booking Order 命令的外部系统是第三方支付软件。
有些领域事件会自动触发下一条命令,我们把自动触发另一条命令的业务规则标识为策略(Policy)。
有些领域事件会通知用户,用户可以通过 UI 手动触发下一条命令。在通知中附带或者在 UI 上显示的和执行下一条命令所需的业务数据。可以标识为读模型(Read Model)。根据读模型,可以识别出 UI 的 mock-up。
在完成设计级事件风暴以后,所有的领域事件都应该如上图所示,识别出关联的明确的领域对象。
以下是笔者对 Booking Order Created 事件识别出关联的设计级领域对象。

对设计级事件风暴,最重要的是识别出聚合(上图黄色的即时贴表示)和 UI mock-up(上图白色的即时贴),UI mock-up 的内容由只读模型决定(上图绿色的即时贴)。更深入分析下去,需要识别聚合根、聚合根上的实体和值对象;如果需要读取或持久化聚合对象,可以识别出资源库(Repository);如果需要组织多个聚合上的领域方法调用或者定义事务范围,需要识别出领域服务。限于时间和篇幅,笔者没有在本文识别出所有的设计级领域对象。
DDD 和事件风暴是实践性很强的软件分析和设计方法,仅仅通过阅读书籍和文章无法真正掌握其精髓。笔者建议大家可以对熟悉的业务,例如做过的项目,尝试用事件风暴进行 DDD 战略和战术建模,实践才是有效学习 DDD 的唯一方法。
关于 DDD 的书籍,以下三本是这个领域的经典著作,现在都能在网上买到。阅读顺序个人建议从《领域驱动设计精粹》入手,这本书很薄,比较浅显易读,更难得的是译者对 DDD 有丰富的实践经验,以注解的形式加了不少自己对原文的理解。
《领域驱动设计-软件核心复杂性应对之道》作者:Eric Evans
《实现领域驱动设计》作者:Vaughn Vernon
《领域驱动设计精粹》作者:Vaughn Vernon




