
2.2 按时间线发现有界上下文
通常边界这个概念是指空间上的边界,例如国土边界。实际上,时间也有边界。时间边界在日常生活中很常见,年、月、日这些时间单位是一种时间刻度,也是时间边界的标记。与资金有关的领域基本都有时间边界,资金的利息是按照时间计算的,企业会计结算转账都是按月的,每年还有年度结算,包括税务领域等也都和时间维度相关。当然,空间边界可能更直接一些,例如物流领域中跟踪的货柜,会在不同地理位置停靠装卸。
为什么选择从时间线进行领域分析呢?因为是在考察业务功能和行为,而功能的发生是在一段时间内的。功能行为的发生也是有先后顺序的,那些发生时间非常靠近的功能行为很有可能是在一个有界上下文里,正如大家开会或谈话,每个人谈论的主题可能是非常相近的。
当从时间线去考察领域的问题空间时,研究每条功能行为背后的职责和目标,它们发生的时间为什么如此接近,是不是为了完成相同的业务规则?如果是,那么就存在逻辑上的一致性,它们应该处于同一个有界上下文中。
可依据下面几点来考察功能行为和有界上下文。
1)如果将此功能移到上下文之外会怎么样?能否完成目标职责?
2)这种上下文是否包含太多业务规则,包含太多的职责和能力?
3)这些职责和能力是否具有凝聚性?是否在时间线上比较接近,只有集聚在一起完成才能保证功能的完整性?
这种方法的关键首先是按照时间顺序对每个功能排序,然后再进入功能内部考察其职责。这个功能的目标是什么?主要关心的是什么?这些都有助于发现业务规则和不变性约束,将逻辑一致的规则约束合并为同类项,也就是合并成一个有界上下文,这是一种通过合并方式划分有界上下文的方式。这种方式的核心还是根据业务规则来判断有界上下文,业务规则本身只是逻辑一致性的另外一种表达而已。
时间线容易与流程混淆,并不是所有业务流程都是按照时间顺序编排的,有的流程可以并行发生,流程也可以组合,在下一章的事件风暴法中,通过发现发生的事件或活动来划分上下文边界,这种方法与流程更相关些,因为事件活动是流程的重要组成部分,而此处谈论的是从纯时间线来探索上下文边界。
2.2.1 UML时序图
DDD建模过程中需要一种语言来描述业务领域,描述方式的不同可能会影响有界上下文的划分,那么如何按时间线描述发生的功能行为呢?当然可以在白板上用一条条线表示功能行为,线条的前后表达功能行为发生的先后,这是一种自然的方式;还可以使用更严格、更标准的方式:UML时序图。
UML是一套面向对象分析设计的统一语言规范,Java、C#是一种程序规范设计语言,而UML是一种广义语言,说通俗一点,Java、C#体现的设计概念和UML的设计概念是一致的,甚至可以相互转换,这也是正向工程或逆向工程的含义所在。
UML和Java、C#虽然都是建模语言,但UML的特点是图形化,成本小,通过绘图工具画图更快速,而Java、C#涉及更多技术细节,成本比较高。依靠UML图可以快速迭代设计概念,推倒重来代价较低,因此使用UML进行DDD的上下文探索发现是一种很便宜的方式。有界上下文是整个系统中最关键、具有战略方向性的设计,如果有界上下文划错了,等同于整个项目的根基发生改变,人员组织团队需要变动,代码库需要重新合并划分,测试交付也需要变动。
UML本身也有缺点,原本是一种简单直观的图形化方式,但是图形化符号太多反而造成初学者的学习门槛提高。
DDD建模基本只需要三种UML图:用例图、序列图和类图。
1)用例图:表达需求用例,也就是将功能需求用图形表达出来。
2)时序图:又称为序列图、顺序图,以可视方式模拟领域中的逻辑流程,能够记录和验证逻辑。时序图和用例图从两个视角来看待需求,时序图主要从逻辑顺序角度理顺需求,而用例图基本是忠实地完整描述功能,表示用例和参与者之间的关系;时列图用于显示对象如何通信,侧重交互通信的顺序关系,可以说是对用例图从时间顺序上的进一步表达。通过依据时间顺序这条线索,能够搞清楚领域中各个功能的前后执行顺序,然后研究这些功能是否可以合并为同类项,当然合并的依据是这些功能行为是否存在逻辑一致性约束。逻辑一致性体现业务规则和不变性约束等几个方面,也就是说,这些功能行为的职责目的是否一致?如果一致就可以合并到一个有界上下文,这样逐步挖掘领域中的各个有界上下文。这些将在下面的电商案例中详细解析。
3)类图:表达类型与结构关系,类似数据表结构,但是有继承、实现等对象表达方式。类图主要用来表达从用例和时序图中推导的领域模型,是建模的最终结果。这将在后面聚合等章节中详细解析。
当然,在实际DDD建模中,不必拘泥于UML的严格表示,可以简单画个框或箭头、线条,写个名字来代表一个对象或事物,只要能代指某个物体即可,并且用统一语言去命名。
2.2.2 实例解析:电商领域之商品管理上下文
本节将以中型电商领域为案例,解析如何通过时间线这条线索,逐条排查各个业务功能,考察每个功能的职责目的所在,挖掘其中的业务规则和不变性约束,将具有逻辑一致性的规则约束合并成同类项,也就是合并成同一个有界上下文,这是一种通过合并方式划分有界上下文的方式。
为简单起见,将电商领域的用例图表达为图2-4所示,这里UML工具使用的是企业架构师EA,也可以使用Rose等其他UML软件工具。
商家在该领域中进行商品管理,用户可浏览商品、下单、支付,商家进行商品发货,这是电商领域主要的几个功能。当然还有更多细节功能,有可能罗列的每个功能都需要一套复杂的用例图来表达。现在主要是介绍一套分析方法,无论分析的对象是简单还是复杂,都可以使用这种方法,以看似简单的电商用例为范本,不会增加初学者的学习难度。

图2-4 电商用例图
现在按照时间线对这些功能进行深入分析。这些功能中的第一步是什么?首先需要执行的功能是什么?答案是:商家的新增商品等功能。网上商店中没有商品肯定无法开张,用户也无法浏览商品,新增商品等功能是先于浏览商品等查看功能发生的,以UML时序图表示为图2-5所示。

图2-5 UML时序图
图中使用“商品管理”代表商品的新增、删除、修改等功能,商家进行“商品管理”的操作这条线高于用户的“浏览商品”这条线,表示了它们发生的先后时间顺序。现在进入这两种功能行为的内部分析,它们是否存在业务策略和规则?商品管理是对商品的新增、修改、查询和删除,简称CRUD;而浏览商品比较简单,主要是查看各种上架的商品,也就是CRUD中的查询;可以发现这两种行为的发生存在一致性,都是围绕“商品”这个主题展开的,如果这些行为中存在业务策略和规则,也应该是围绕商品展开的,因此这两种行为并不是两种独立的行为,是在逻辑上一致的,将这种具有行为一致性的逻辑封装在一个有界上下文内。
那么,这种一致性体现在哪里?可以引入一个新的对象类型:XX组件,由该组件提供逻辑一致性体现的地方。这个组件到底是什么?接下来需要给它命名。根据DDD的统一语言规则,名称是大家约定的术语,前面已经讨论过,这两种行为或职责是围绕“商品”展开的,这是它们体现行为一致性的地方,这个组件应该与“商品”这个统一术语有关。与“商品”有关的术语很多,如“产品”“物品”“抵押品”等,这里还是需要根据电商所处的具体行业来进行选择,可以使用比较通用的“商品”这个术语,也是当前这个案例上下文的约定用语。
那么这个组件是不是应该命名为“商品组件”?不是,因为“商品组件”不能表达“提供”这个动词的意思,准确的表达应该是“提供商品操作功能的组件”。这里又涉及另外一个统一用语,“提供操作功能”可以用什么术语表达呢?根据OASIS定义,“服务”是一种允许访问一个或多个功能的机制,也就是提供操作功能以便于被访问,那么,“提供商品操作的功能”可以表达为“商品服务组件”。这样就完成了一个有界上下文的划分以及服务的命名,如图2-6所示。

图2-6 命名组件
关于“服务”和“商品”的区别在现实生活中比比皆是,例如:加油站是提供加油服务的站点,不是只有油品商品的站点,如果只有油品商品,那么它应该叫仓库或仓储;人们到餐馆吃饭,服务员是提供餐馆服务的。
可以看出,因为服务的存在,才有了商品的应用场景,也就是商品的应用上下文,所以,服务是有可能和有界上下文重合的。
回到电商案例,现在已经将商品管理功能划分成一个单独的有界上下文,那么下单、支付和发货功能如何分析?下单是用户下订单,支付时用户需要根据支付方式进行支付,这两种行为的关注点是否一致呢?如果存在业务策略和规则,是否存在一致性?
下单的重点是保证订单成功生成,如果用户需要修改订单中的商品,可能只能通过购物车修改,然后重新生成新的订单;一旦一份订单生成就不可修改,因为涉及后续的支付、发货流程,如果能够变化,后续流程无法进行;订单的总金额必须是订单中各项商品数量与单价乘积的总和。这些都是其业务规则,或者称为不变性约束。
支付的重点是如何保证支付成功,用户账户里面的余额是否足够支付,与银行系统的接口API访问授权处理……这些都是支付的职责操作和业务规则。
这两种业务规则是完全不同的,不存在逻辑一致性,因此可以判断是两个不同的有界上下文。同理,发货也是不同于下单、支付的有界上下文。这样,有了三个有界上下文:订单、支付和发货。
现在开始设计订单有界上下文。在这个上下文中,主要任务是实现订单的生成,完成用户下订单的命令,表达用户的订购活动,当然,有的人可能认为,这个有界上下文不一定取名为“订单”,“交易”也比较合适,那么就将这里的有界上下文通过“订单/交易服务组件”来支持,如图2-7所示。

图2-7 两个有界上下文
订单/交易服务组件支持的功能:用户下单、支付和发货。为了体现这些命令/事件是围绕订单而凝聚发生的,具有一致的逻辑性,订单上下文中的支付可能不是真正的支付功能,因为真正支付是处于另外一个上下文的支付服务中,这里的支付是针对订单状态的修改,当用户真正支付完成后,通过此支付方法将订单状态修改为已支付。
同样,发货方法也不是真正的发货,通过此方法将订单状态修改为已发货。这里涉及订单上下文、支付上下文和发货上下文之间的关系调用,通过发布/订阅模式,由支付上下文和发货上下文发布支付或发货事件到订单上下文。
图2-7中还有商品服务这个有界上下文,这样做的好处是,可以从整个时序图中鸟瞰所有功能发生的时间顺序,从而能不断迭代调整上下文的边界。这是一个迭代、动态的变化过程,有界上下文不是一次设计完成,设计好了就放到另外一张图中,还要将不同上下文纳入整个领域背景下串联起来,看看是否能完整覆盖整个领域边界。这是一种领域流故事法(Domain Flow Storytelling),有界上下文本身就是故事中的演员。因此,故事始于用户实现的功能目标,然后是有界上下文之间的交互,检查这样能否最终提供整个解决方案。
通过时间顺序考察每个功能行为的发生情况,发现其中规则和职责目的是否存在一致性,进而界定出有界上下文,这是本节演示的一个重点。在后面的“聚合”等章节中将继续细化这个案例,考察领域模型的提炼,并使用类图来实现。