[DDD读书笔记] 战略设计①模型上下文策略

396 阅读18分钟

前文回顾

上一篇介绍了该书的第三部分“通过重构来加深理解”,我们学习了分析模式和设计模式在模型设计上的应用,同时归纳了在代码重构时需要注意的地方。

这一篇,我们开始学习该书的第四部分。

何谓战略设计

在几乎所有的大型企业中,整体业务模型太大也太复杂了,因此难以管理,甚至很难把它作为一个整体来理解。我们必须在概念和实现上把系统分解为较小的部分,所谓化整为零,分而治之。

如何在模块化的同时,不让系统的整体模型产生割裂?战略设计正是应对这个问题的设计策略。战略设计的原则必须指导设计决策,以便减少各部分之间的依赖,同时必须把模型的重点放在捕获系统的概念核心,也就是系统的“远景”上。而且在完成这些目标的同时又不能为项目带来麻烦。为了帮助实现这些目标,这一部分探索了3个大的主题:上下文、精炼和大型结构。其中上下文是最不易引起注意的原则,但实际上它却是最根本的。

通过为每个模型显式地定义一个BOUNDED CONTEXT,然后在必要的情况下定义它与其他上下文的关系,建模人员就可以避免模型变得缠杂不清。通过精炼可以减少混乱,并且把注意力集中到正确的地方。大型结构是用来描述整个系统的。在非常复杂的模型中,人们可能会“只见树木,不见森林”。

大型结构和精炼能够帮助我们理解各个部分之间的复杂关系,同时保持整体视图的清晰。BOUNDED CONTEXT使我们能够在不同的部分中进行工作,而不会破坏模型或是无意间导致模型的分裂。

模式:BOUNDED CONTEXT

模型最基本的要求是它应该保持内部一致,术语总具有相同的意义,并且不包含互相矛盾的规则。但在大型企业中,很难达到全局的高度统一。除了技术上的因素以外,权力上的划分和管理级别的不同也可能要求把模型分开。而且不同模型的出现也可能是团队组织和开发过程导致的结果。

既然无法维护一个覆盖整个企业的统一模型,那么我们就需要知道哪些应该统一,哪些不需要。我们需要用一种方式来标记出不同模型之间的边界和关系,同时还需要有意识地选择一种策略,并遵守它。

image.png 细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。

任何大型项目都会存在多个模型,而当基于不同模型的代码被组合到一起后,软件就会出现bug、变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型应该在哪个上下文中使用。模型混乱的根源在于团队的组织方式和成员的交流方法。为了避免这种问题,需要保证一个模型只在一个上下文中使用

MODULE可以用来划分模型。当两组对象组成两个不同模型时,人们几乎总是把它们放在不同的MODULE中。但人们也会在同一个模型中用MODULE来组织元素,它们不一定要表达划分CONTEXT的意图。

那么通过定义BOUNDED CONTEXT,最终可以得到什么呢?对CONTEXT内的团队而言:清晰!。这两支团队知道他们必须与这个模型保持一致。他们根据这一点制定设计决策,并注意防范出现不一致的情况。而CONTEXT之外的团队获得了:自由。他们不必行走在灰色地带,不必使用同一个模型

模式:CONTINUOUS INTEGRATION

需要注意将不同模型的元素组合到一起时可能会引发两类问题:概念重复和假同源。概念重复是指两个模型元素(以及伴随的实现)实际上表示同一个概念。假同源可能稍微少见一点,但它潜在的危害更大。它是指使用相同术语(或已实现的对象)的两个人认为他们是在谈论同一件事情,但实际上并不是这样。

当很多人在同一个BOUNDED CONTEXT中工作时,模型很容易发生分裂。团队越大,问题就越大,但即使是3、4个人的团队也有可能会遇到严重的问题。然而,如果将系统分解为更小的CONTEXT,最终又难以保持集成度和一致性。

CONTINUOUS INTEGRATION(持续集成)模式旨在建立一个把所有代码和其他实现工件频繁地合并到一起的过程,并通过自动化测试来快速查明模型的分裂问题。严格坚持使用UBIQUITOUS LANGUAGE,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识。

CONTINUOUS INTEGRATION只有在BOUNDED CONTEXT中才是重要的。相邻CONTEXT中的设计问题(包括转换)不必以同一个步调来处理。

模式:CONTEXT MAP

image.png

当其他团队中的人员并不是十分清楚CONTEXT的边界时,他们会不知不觉地做出一些更改,从而使边界变得模糊或者使互连变得复杂。当不同的上下文必须互相连接时,它们可能会互相重叠。

CONTEXT MAP既属于项目管理,又是软件设计的一部分。它必须为每个BOUNDED CONTEXT提供一个明确的名称,而且必须阐明联系点和它们的本质。记住,CONTEXT MAP始终表示当前的情况,模型设计可能发生修改,在修改实际完成之前,不要更新CONTEXT MAP。另外,在测试中,对各个BOUNDED CONTEXT的联系点的测试特别重要。

关于CONTEXT MAP的组织和文档化有以下两个重点。

  1. BOUNDED CONTEXT应该有名称,以便可以讨论它们。这些名称应该被添加到团队的UBIQUITOUS LANGUAGE中。
  2. 每个人都应该知道边界在哪里,而且应该能够分辨出任何代码段的CONTEXT,或任何情况的CONTEXT。

限界上下文之间的关系

开发一个紧密集成产品的优秀团队可以部署一个大的、统一的模型。如果团队需要为不同的用户群提供服务,或者团队的协调能力有限,可能就需要采用SHARED KERNEL(共享内核)或CUSTOMER/SUPPLIER(客户/供应商)关系。有时仔细研究需求之后可能发现集成并不重要,而系统最好采用SEPARATE WAY(各行其道)模式。当然,大多数项目都需要与遗留系统或外部系统进行一定程度的集成,这就需要使用OPEN HOST SERVICE(开放主机服务)或ANTI-CORRUPTION LAYER(防腐层)。

模式:SHARED KERNEL

image.png

从领域模型中选出两个团队都同意共享的一个子集。当然,除了这个模型子集以外,还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位,一个团队不能在没与另一个团队商量的情况下擅自更改。功能系统要经常进行集成,但集成的频率应该比团队中CONTINUOUS INTEGRATION的频率低一些。在进行这些集成的时候,两个团队都要运行测试。

SHARED KERNEL通常是CORE DOMAIN,或是一组GENERIC SUBDOMAIN(通用子领域)。

模式:CUSTOMER/SUPPLIER DEVELOPMENT TEAM

如果下游团队对变更具有否决权,或请求变更的程序太复杂,那么上游团队的开发自由度就会受到限制。由于担心破坏下游系统,上游团队的活动甚至会受到抑制。同时,由于上游团队掌握优先权,下游团队有时也会无能为力。正式规定团队之间的关系会使所有人工作起来更容易。这样,就可以对开发过程进行组织,均衡地处理两个用户群的需求,并根据下游所需的特性来安排工作。

可以在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。两个团队共同开发自动化验收测试,用来验证预期的接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对下游团队产生副作用。

建立正式的需求响应流程对双方都有利,这种模式有两个关键要素。

  1. 这种团队关系是客户与供应商的关系,也就是说客户侧的需求是至关重要的。下游团队不必乞求上游团队满足其需求。
  2. 必须有自动测试套件,使上游团队在修改代码时不必担心破坏下游团队的工作,并使下游团队能够专注于自己的工作,而不用总是密切关注上游团队的行动。

当上下游团队不归同一个管理者指挥时,上游团队没有动力满足下游团队的需求,上游开发人员即使做出承诺也可能不会履行,这时客户/供应商模式的合作很难奏效。有3种解决途径:

  1. 如果上游软件并没有太高的价值,完全可以放弃对上游的使用,即SEPARATE WAY(各行其道)。
  2. 如果上游软件有很高的价值,但模型很难使用的话,可以采用ANTI-CORRUPTION LAYER(防腐层),将上下游模型隔离。
  3. 如果上游价值高,设计优良且风格兼容的话,下游没必要开发独立的模型,可以采用CONFORMIST(跟随者)模式。

模式:CONFORMIST

当使用一个具有很大接口的现成组件时,一般应该遵循(CONFORM)该组件中隐含的模型。组件和你自己的应用程序显然是不同的BOUNDED CONTEXT,因此根据团队组织和控制的不同,可能需要使用适配器来进行一点点格式转换,但模型一定要保持相同

通过严格遵从上游团队的模型,可以消除在BOUNDED CONTEXT之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择CONFORMIST模式可以极大地简化集成。此外,这样还可以与供应商团队共享UBIQUITOUS LANGUAGE。

CONFORMIST模式类似于SHARED KERNEL模式。在这两种模式中,都有一个重叠的区域——在这个重叠区域内模型是相同的,此外还有你的模型所扩展的部分,以及另一个模型对你没有影响的部分。这两种模式之间的区别在于决策制定和开发过程不同

模式:ANTI-CORRUPTION LAYER

image.png

创建一个隔离层,以便根据客户自己的领域模型来提供相关功能。这个层通过调用另一个系统现有的接口与其进行对话。在内部,这个层在两个模型之间进行必要的双向转换。ANTI-CORRUPTION LAYER并不是向另一个系统发送消息的机制。相反,它是在不同的模型和协议之间转换概念对象和操作的机制

ANTICORRUPTION LAYER的公共接口通常以一组SERVICE的形式出现,但偶尔也会采用ENTITY的形式。可以采用FACADE、ADAPTER模式和转换器的组合来实现防腐层。

image.png 如果FACADE无法直接调用另一个子系统,可能要在FACADE和另一个子系统之间设置通信链接。但是,如果FACADE可以直接与另一个子系统集成到一起,那么在适配器和FACADE之间设置通信链接也不失为一种好的选择,这是因为FACADE的协议比它所封装的内容要简单。

模式:SEPARATE WAY

集成总是代价高昂,而有时获益却很小。仅仅因为特性在用例中相关,并不一定意味着它们必须集成到一起。因此:声明一个与其他上下文毫无关联的BOUNDED CONTEXT,使开发人员能够在这个小范围内找到简单、专用的解决方案。特性仍然可以被组织到中间件或UI层中,但它们将没有共享的逻辑,而且应该把通过转换层进行的数据传输减至最小,最好是没有数据传输

模式:OPEN HOST SERVICE

要想设计出一个足够干净的协议,使之能够被多个团队理解和使用,是一件十分困难的事情,因此只有当子系统的资源可以被描述为一组内聚的SERVICE并且必须进行很多集成的时候,才值得这样做

因此:定义一个协议,把你的子系统作为一组SERVICE供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。对于特殊需求应该采用一次性的转换器来扩充协议,以便使共享协议保持简单且内聚。

模式:PUBLISHED LANGUAGE

当采用OPEN HOST SERVICE模式时,其他团队不得不学习OPEN HOST团队使用的专用术语。有些情况下,采用一个众所周知的PUBLISHIED LANGUAGE(公开发布的语言)作为交换模型可以减少耦合并简化理解

PUBLISHED LANGUAGE模式是把一个文档化良好的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。例如在化学领域,有很多程序用来分析处理化学公式,但它们使用不同的模型来表达化学结构,因此很难做到数据交换。为了解决这个问题,作为化学领域的公开发布语言CML(化学标记语言)被开发出来了,下图是CML的一个样例。

image.png

选择模型上下文策略

我们都听说过盲人摸象的故事,大多数人是当做笑话来听的,但对同一个事物存在不同的认识是完全合理的。事实上,承认多个互相冲突的领域模型正是面对现实的做法。通过定义模型所处的上下文,可以维护模型内部的完整性,同时可以看清模型之间的接口。

在选择模型上下文策略的时候,团队首先要决定在哪里定义BOUNDED CONTEXT,以及它们之间有什么样的关系。当选择较大粒度的BOUNDED CONTEXT时,一个内聚统一的模型比起多个模型加上它们直接的映射,更容易理解,但与外部模型间的转换会很难,而且较大的CONTEXT要求更通用更抽象的模型,对建模人员的能力要求更高。当选择较小粒度的BOUNDED CONTEXT时,团队规模和代码规模都比较小,更容易实现持续集成,而且成员间沟通开销也会减少。

在考虑与外部系统的关系时,可以考虑SEPARATE WAY模式,如果集成确实非常重要,那么可以考虑CONFORMIST模式或者ANTI-CORRUPTION LAYER模式。当对一个大型系统进行扩展,而且这个系统还是主要系统时,选择继续使用遗留模型就非常合适。当与另一个系统的接口很少或者另一个系统的设计很糟糕时,可以考虑使用自己的模型,这时需要构筑一个防腐层。

对于设计中的系统,可以在整个系统中使用一个BOUNDED CONTEXT。例如,当一个少于10人的团队正在开发高度相关的功能时,这可能就是一种很好的选择。

随着团队规模的增大,CONTINUOUS INTEGRATION可能会变得困难。在这些BOUNDED CONTEXT中,如果有两个上下文之间的所有依赖都是单向的,就可以建成CUSTOMER/SUPPLIER DEVELOPMENT TEAM。

如果两个团队的思想截然不同,也可以采用SEPARATE WAY模式,在需要集成的地方,两个团队可以共同开发并维护一个转换层,把它作为唯一的CONTINUOUS INTEGRATION点。这与同外部系统的集成正好相反,在外部集成中,一般由ANTI-CORRUPTION LAYER来起调节作用,而且从另一端得不到太多的支持。

只有经过大量开发工作和知识消化之后,深层次模型才会在生命周期的后期出现。深层次模型不是计划出来的,我们只能在它出现的时候抓住机遇,修改自己的策略并进行重构。

下图是CONTEXT关系模式的相对要求: image.png

策略间的转换

合并CONTEXT:SEPARATE WAY -> SHARED KERNEL

合并BOUNDED CONTEXT的动机很多,即使最终目标是完全合并成一个采用CONTINUOUS INTEGRATION的CONTEXT,也应该先过渡到SHARED KERNEL。以下是一个合并流程示例:

  1. 首先应当建立合并过程。需要决定代码的共享方式以及模块应该采用哪种命名约定,以及多久集成一次。
  2. 然后选择某个小的子领域作为开始,它应该是两个CONTEXT中重复出现的子领域,但不是CORE DOMAIN的一部分。
  3. 从两个团队中共选出2~4位开发人员组成一个小组,由他们来为子领域开发一个共享的模型。他们需要识别出同义词,映射和尚未被翻译的术语,还需要为模型开发一个基本的测试集。
  4. 来自两个团队的开发人员一起负责实现模型(或修改要共享的现有代码)、确定各种细节并使模型开始工作。
  5. 每个团队的开发人员都承担与新的SHARED KERNEL集成的任务。
  6. 最后清除那些不再需要的翻译。

合并CONTEXT:SHARED KERNEL -> CONTINUOUS INTEGRATION

一旦进入到合并CORE DOMAIN的过程中,最好能快速完成。这是一个开销高且易出错的阶段,因此应该尽可能缩短时间,要优先于新的开发任务。但注意量力而行,不要超过你的处理能力。 随着SHARED KERNEL的增长,把集成频率提高到每天一次,最后实现CONTINUOUS INTEGRATION。

淘汰遗留系统

有趣的是,遗留系统设计得越好,它就越容易被淘汰。而设计得不好的软件却很难一点儿一点儿地去除。 在其他条件都相同的情况下,应该首先迁移那些只产生较小ANTI-CORRUPTION LAYER的功能。

OPEN HOST SERVICE -> PUBLISHED LANGUAGE

如果有一种行业标准语言可用,则尽可能评估并使用它。如果没有标准语言或预先公开发布的语言,则完善作为HOST的系统的CORE DOMAIN

如果保持一个大的BOUNDED CONTEXT能够解决迫切的集成需要,而且除了模型本身的复杂性以外,这看上去是行得通的,那么分割CONTEXT可能就不是最佳的选择了。

系列文章