《实现领域驱动设计》读书笔记(2) - 战略建模

系列大纲: 《实现领域驱动设计》读书笔记

本文大纲:

通用语言(Ubiquitous Language)

通用语言是团队成员间能够互相理解的语言,它统一了开发团队和领域专家之间的术语体系,从而提高团队成员之间的沟通效率。通用语言不同于「**统一建模语言(UML, Unified Modelling Language)**」,UML 是开发人员之间的语言。


通用语言的几点注意:

  • 有界上下文和通用语言存在一对一的关系
  • 只有当团队工作在独立的有界上下文中时,通用语言才是「通用」的
  • 虽然我们只工作在一个有界上下文中,但我们可能经常会与其他有界上下文打交道,这时可以通过上下文映射图对这些有界上下文进行集成,每个有界上下文都有自己的通用语言,而有时语言间的术语可能有重叠的部分

领域和子域

在 DDD 中,一个领域可能包含多个有界上下文,通常一个有界上下文对应一个领域。「领域」一词承载了太多含义,领域既可以表示整个业务系统,也可以表示其中的某个核心域或支撑子域,域模型在特定的域中应该表达出清晰的含义。

例如,在一个电子商务系统中,至少要向买家展示不同类别的产品,允许买家下单和付款,还需要安排物流。这个特定的领域可以划分为「**产品目录(Product Catalog)」,「订单(Order)」,「发票(Invoicing)」和「物流(Shipping)」。子域不一定会很大,可以简单到只包含一套算法,这套算法对业务系统来说可能非常重要,但并不包含在核心域之中。在正确实施 DDD 的情况下,这种简单的子域可以以「模块(Module)**」的形式从核心域中分离出来。

核心域

通常代表了促进业务成功的核心功能,核心域应该配备最好的领域专家和开发团队。

支撑子域

如果一个有界上下文对应着业务的某些重要方面,但却不是核心,那么它便是一个「支撑子域」。

通用子域

如果一个子域被用于整个业务系统,那么这个子域便是「通用子域」。例如在众多系统中都有的「认证与授权子系统」通常就是一个通用子域。

问题空间(Problem Space)和解决方案空间(Solution Space)

  • 问题空间是领域的一部分,对问题空间的开发将产生一个新的核心域,对问题空间的评估应该同时考虑已有子域和额外所需子域。因此,问题空间是核心域和其他子域的组合。
  • 解决方案空间包括一个或多个有界上下文,因为有界上下文即是一个特定的解决方案。

在我们实施某个解决方案之前,我们需要对问题空间和解决方案空间进行评估,首先回答以下问题:

  • 这个战略核心域的名字是什么?
  • 它的目标是什么?
  • 它包含哪些概念?
  • 它的支撑子域和通用子域是什么?
  • 如何安排项目人员?
  • 你能组建一支合适的团队吗?

解决方案空间在很大程度上受到现有系统和技术的影响。我们应该根据有界上下文仔细考虑以下问题:

  • 有哪些软件资产是已经存在的,它们可以重用吗?
  • 哪些资产是需要创建的,或者从别处获得?
  • 这些资产是如何集成在一起的?
  • 还需要什么样的集成?
  • 假设已经有了现有资产和那些需要被创建的资产,我们还需要做些什么?
  • 核心域和那些支撑项目的成功几率如何?会不会出现由于其中一个失败而导致整个项目失败的可能?
  • 有哪些地方我们使用了完全不同的术语?
  • 有界上下文之间在哪些地方存在概念重叠?
  • 这些重叠的概念在不同的有界上下文之间是如何映射和翻译的?
  • 哪些有界上下文包含了核心域中的概念,其中使用了哪些战术建模工具?

有界上下文(Bounded Context)

有界上下文采用 模型+上下文 的形式来命名。

同一个概念在不同的有界上下文中的关注点是不一样的,例如在一个电子商务系统中,「顾客」这个概念在订单系统上下文中,其关注点可能有先前购买情况,忠诚度,可买产品,折扣和物流方式,而在下单时,「顾客」的上下文包括名字,产品寄送地址,订单总价和一些付款术语。所以「顾客」在这个案例中并没有一个清晰的含义。类似的问题其实是脱离了不同有界上下文中协作概念的关注点,在不同的有界上下文中,「顾客」扮演了不同的协作概念,例如在产品目录上下文中,「顾客」可以用「浏览者」表示,而在订单上下文中,「顾客」以「购买者」表示。「顾客」一词包含了太多可能的角色,不同的角色有不同的职责。在不同的有界上下文中,不同角色充当了「顾客」的某种职责,进而使得单一有界上下文该角色的含义清晰,并与其他有界上下文的关注点得以分离。

有界上下文是一个显式的语义边界,领域模型便存在于这个边界之内。领域模型把通用语言表达成软件模型,创建边界的原因在于,每一个模型概念,包括它的属性和行为,在边界之内都具有特殊的含义。

在上下文边界之外,我们通常不会使用该上下文之内的对象实例,但是不同上下文中彼此关联的对象可能共享一些状态。

当模型驱动着数据库 Schema 的设计时,数据库 Schema 也应该位于该模型所处的上下文边界之内。因为数据库 Schema 是由建模团队设计,开发并维护的。这也意味着数据库中表和列的名字应该和模型的名字保持一致。另一方面,如果数据库 Schema 已经存在,或者另有一个专门的数据建模团队要求有别于模型的数据库 Schema 设计,此时的 Schema 便不能和模型位于同一个有界上下文中了。

如果「用户界面(UI)」被用于渲染模型,并且驱动着模型的行为设计时,同样,该用户界面也应该属于模型所在的上下文边界之内。但是,这并不表示我们应该在用户界面中对领域进行建模,因为这样将导致「贫血领域对象」或者任何试图将领域概念带到领域模型之外的举措。

通常情况下,一个系统/应用程序的使用者并不只是人,还可能是另外的计算机系统。系统中有可能存在诸如 Web 服务之类的组件,或者使用 REST 资源来与模型交互,在所有可能的情形下,这些面向服务的组件都应该位于上下文边界之内。

用户界面和面向服务的端点都会将操作委派给「**应用服务(Application Service)**」,应用服务包含了不同类型的服务,比如安全和事务管理等。对于模型来说,应用服务扮演的是一种门面模式(Facade)。同时,应用服务还具有管理功能,它将来自用例流(Use Case Flow)的请求转换成领域逻辑的执行流。应用服务也是位于上下文边界之内的。

有界上下文主要用来封装通用语言和领域对象,但同时它也包含了那些为领域模型提供交互手段和辅助功能的内容。需要注意的是,对于架构中的每个组件,我们都应该将其放在适当的地方。有界上下文可以包含「**模块(Module)」,「聚合(Aggregate)」,「领域事件(Domain Event)」和「领域服务(Domain Service)**」。有界上下文应该足够大,以表达它所对应的整套通用语言。

上下文映射图

上下文映射图表示了不同有界上下文之间是如何集成的,任何两个有界上下文可能存在某种模式:

  • 合作关系(Partnership): 两个团队各自负责自己的上下文,在接口的演化上进行合作以同时满足两个系统的需求
  • 共享内核(Shared Kernel): 两个上下文对模型和代码的共享产生一种紧密的依赖性,需要为共享的部分指定一个显式的边界,并保持共享内核的最小化。在没有与另一个团队协商的情况下,共享内核是不能改变的。应该引入一种持续集成机制来保证共享内核与通用语言的一致性。
  • 客户方-供应方开发(Customer-Supplier Development): 两个上下文处于上-下游关系,上游团队独立于下游团队完成开发,下游团队的开发可能会受到很大的影响。因此在上游团队的计划中,应该顾及下游团队的需求。
  • 遵奉者(Confirmist): 在存在上-下游关系的两个团队中,上游团队完全不考虑下游团队的需求,而下游团队只能盲目地使用上游团队的模型。
  • 防腐层(Anticorruption Layer): ACL,当两个上下文不是合作共享内核或者客户-供应方关系时,翻译将变得复杂。下游团队需要根据自己的领域模型创建一个单独的层,该层作为上游系统的代理提供功能。防腐层通过已有的接口与其他系统交互,在防腐层内部,在自己的模型和他方的模型之间进行翻译转换。如果翻译过于复杂,并且需要大量的数据复制和同步,从而使得翻译前后的模型存在很大的相似度,那么你可能过多地使用了外部上下文中的数据,导致自己的模型混淆不清了。
  • 开放主机服务(Open Host Service): OHS,定义一种协议,其他系统通过该协议来访问该系统,协议是公开的,这样任何想与这个系统集成的人都可以使用该协议。通常来讲,我们可以将开发主机服务看成是远程过程调用 (Remote Procedure Call) 的 API。同时,它也可以通过消息机制实现。
  • 发布语言(Published Language): PL,在两个有界上下文之间翻译模型需要一种公用的语言。此时你应该使用一种发布出来的共享语言来完成集成交流。发布语言通常与开放主机服务一起使用,常见的发布语言使用 XML Schema。在使用 REST 服务时,可以使用 XML 和 JSON,也可以使用 Google Proto Buffer 来表示。使用 REST 的好处是每个客户端都可以指明使用哪种语言,同时还可以指明资源的展现方法。
  • 另谋他路(SeperateWay): 如果两套系统之间没有任何显著的关系,那么他们是完全解耦的,集成总是昂贵的。
  • 大泥球(Big Ball of Mud): 当我们检查已有系统时,经常会发现系统中存在混杂在一起的模型,它们之间的边界非常模糊,此时应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列。

系统间集成经常依赖于 RPC。RPC 与编程语言中的过程调用非常相似。和在相同进程中的过程调用不同的是,RPC 更容易产生有损性能的时间延迟,并有可能导致调用彻底失败。虽然 REST 并不是真正意义上的 RPC,但它却具有与 RPC 相似的特征。然而,要与远程模型保持同步,最好的方式是在远程系统中采用面向消息的通知机制(例如 RabbitMQ)。消息通知可以通过服务总线进行发布,也可以采用消息队列或者 REST。