前文回顾
上一篇介绍了该书的第四部分“精炼”,我们学习了可以用于精炼的几种模式,比如 CORE DOMAIN 模式, GENERIC SUBDOMAIN 模式, DOMAIN VISION STATEMENT 模式等等。
这一篇,我们继续学习该书的第四部分。
大型结构
在一个大的系统中,如果因为缺少一种全局性的原则而致使人们无法根据元素在模式中的角色来解释这些元素,那么开发人员就会陷入“只见树木,不见森林”的境地。
设计一种应用于整个系统的规则(或角色和关系)模式,使人们可以通过它在一定程度上了解各个部分在整体中所处的位置,即使不知道各个部分的详细职责。这种结构通常会跨越多个BOUNDED CONTEXT。
模式: ENVOLVING ORDER
一个没有任何规则的随意设计会产生一些无法理解整体含义且很难维护的系统。但在中早期的设计规则又会使项目变得束手束脚,而且会极大地限制应用程序中某些特定部分的开发人员/设计人员的能力。很快,开发人员就会为适应规则而不得不在程序的开发上委曲求全,要么就是完全推翻架构而又回到没有协调的开发老路上来。
问题并不在于指导规则本身应不应该存在,而在于这些规则的严格性和来源。如果这些用于控制设计的规则确实符合开发环境,那么它们不但不会阻碍开发,而且还会推动开发在健康的方向上前进,并且保持开发的一致性。因此:让这种概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同的结构风格。不要依此过分限制详细的设计和模型决策,这些决策和模型决策必须在掌握了详细知识之后才能确定。
在选择大型结构时,应该侧重于整体模型的管理,而不是优化个别部分的结构。因此,在“结构统一”和“用最自然的方式表示个别组件”之间需要做出一些折中选择。
当发现一种大型结构可以明显使系统变得更清晰,而又没有对模型开发施加一些不自然的约束时,就应该采用这种结构。
模式: SYSTEM METAPHOR
SYSTEM METAPHOR(系统隐喻)是一种松散的、易于理解的大型结构,它与对象范式是协调的。由于系统隐喻只是对领域的一种类比,因此不同模型可以用近似的方式来与它关联,这使得人们能够在多个BOUNDED CONTEXT中使用系统隐喻,从而有助于协调各个BOUNDED CONTEXT之间的工作。
当系统的一个具体类比正好符合团队成员对系统的想象,并且能够引导他们向着一个有用的方向进行思考时,就应该把这个类比用作一种大型结构。“防火墙”就是一个类比,真实世界的防火墙可以防止火势从其他建筑蔓延到自身,而软件“防火墙”可以保护局域网免受来自外部网络的破坏。
模式: RESPONSIBILITY LAYER
如果每个对象的职责都是人为分配的,将没有统一的指导原则和一致性,也无法把领域作为一个整体来处理。为了保持大模型的一致,有必要在职责分配上实施一定的结构化控制。
注意观察模型中的概念依赖性,以及领域中不同部分的变化频率和变化的原因。如果在领域中发现了自然的层次结构,就把它们转换为宽泛的抽象职责。这些职责应该描述系统的高层目的和设计。对模型进行重构,使得每个领域对象、AGGREGATE和MODULE的职责都清晰地位于一个职责层当中。
以运输系统为例,以下是运输系统模型的一部分。此时开发团队已经提炼出一个CORE DOMAIN,他们正在寻找一个能体现整个系统的主题并且让大家一致认同的大型结构。
下图展示了在预订期间如何使用模型来制定一个货运线路。
团队成员观察到了一些自然的概念层次结构,比如讨论运输时间表时,不需要涉及到货物。而讨论货物的跟踪时,如果不知道它的运输信息,就很难进行跟踪。这里有很清晰的概念依赖性。团队分出了两个层:作业层和支持这些作业的基础层(也称为能力层)。
公司的活动都被放到作业层中,最明显的作业对象是 Cargo ,它是公司大部分日常活动的焦点。Route Specification 是 Cargo 不可或缺的一部分,它规定了运输需求,而 Itinerary 是运输计划,这些对象都以 Cargo 为聚合根。
能力层反应了公司在执行作业时所能利用的资源。为货轮制定航程时间表时,需要考虑货轮的货运能力,因此 Transport Leg 是一个典型的例子。
把 Customer 对象放在哪里是一个稍微复杂一点决策。这家运输公司需要与客户保持长期关系,因为大部分业务来自回头客。所以 Customer 应该属于能力层。如果是快递公司,客户可能只是临时对象,在包裹投递完成后就被遗忘了,此时客户仅与作业相关,可以放在作业层。
经过几个星期的试验后,团队发现 Router 并不是作业(计划)的一部分,它是用来帮助修改计划的,因此定义了一个新的层,用来支持决策(Decision Support)。
决策支持层可以为用户提供用于制定计划和决策的工具。Router 是一个 SERVICE,能帮助选择运送货物的最佳路线,因此属于决策支持层。原先在 Transport Leg 中的 is preferred 属性是因为公司希望优先使用自己的货轮,当 Router 选择最佳路线时会用到这个属性,因此它与能力层毫无关系,所以最终被重构到了 Route Bias Policy 中。
在多次重构之后这个模型更加符合大型结构了。
在我们选择合适的 RESPONSIBILITY LAYER 时,可从以下几个方面考量。
场景描述
层应该能够表达出领域的基本现实或优先级。选择一种大比例结构与其说是一种技术决策,不如说是一种业务建模决策。层应该显示出业务的优先级。
概念依赖性
较高层概念的意义应该依赖较低层,而底层概念应该独立于较高的层。
以下是常见的几种分层。
能力层
我们能够做什么?能力层不关心我们打算做什么,而关心能够做什么。也称为潜能层。
作业层
我们正在做什么?我们利用这些潜能做了什么事情?像潜能层一样,这个层也应该反映出现实状况,而不是我们设想的状况。我们希望在这个层中看到自己的工作和活动:我们正在销售什么,而不是能够销售什么。
决策支持层
应该采用什么行动或指定什么策略?这个层是用来作出分析和制定决策的。它根据来自较低层(比如潜能层或作业层)的信息进行分析。决策支持软件可以利用历史信息来主动寻找适用于当前和未来作业的机会。 很多项目利用数据仓库技术实现决策支持层,此时决策支持层实际上变成了一个独特的BOUNDED CONTEXT,并且与作业层软件具有一种CUSTOMER/SUPPLIER关系。
策略层
规则和目标是什么?策略层可以和其他层使用同一种语言来编写,但他们有时候是使用规则引擎来实现的。所以也可以考虑放到单独的 BOUNDED CONTEXT 中。
承诺层
我们承诺了什么?这个层具有策略层的性质,因为它表述了一些指导未来运营的目标;但它也有作业层的性质,因为承诺是作为后续业务活动的一部分而出现和变化的。
上述5个层虽然对很多企业系统都适用,但并不是所有领域的概念都涵盖在这5个层中。我们需要根据实际情况决定分层,层数过多将无法有效描述领域。
模式: KNOWLEDGE LEVEL
KNOWLEDGE LEVEL 是一组描述了另一组对象应该有哪些行为的现象。它并不像其他分析模式那样对领域进行建模,而是用来构造模型的。
当对系统进行修改或替换时,开发人员会发现,有一些功能的真实含义并不想他们看上去的那样。它们在不同情况下具有完全不同的含义。如果不破坏这些互相叠加的含义,修改任何东西都是非常困难的。想要把数据迁移到一个“更合适”的系统中,必须要理解这些奇怪的部分,并对其进行编码。
如果在一个应用程序中,ENTITY的角色和它们之间的关系在不同的情况下有很大变化,那么复杂性会显著增加。这种情况下,无论是一般的模型还是高度定制的模型,都无法满足用户的需求。为了兼顾各种不同的情形,对象需要引用其他的类型,或者需要具备一些属性,用于在不同情况下采用不同使用方式。相当于在我们模型中嵌入了另一个模型,作用是用来描述我们的模型。KNOWLEDGE LEVEL 分离了模型的自我定义的功能,并清晰地表达了它的限制。
KNOWLEDGE LEVEL 模式下,需要创建一组不同的对象,用他们来描述和约束基本模型的结构和行为。把这些对象分为两个“级别”,一个是非常具体的级别,另一个级别则提供了一些可供用户或者超级用户定制的规则和知识。
以员工工资和退休金管理系统为例,员工分为小时工和受薪员工,小时工的退休金计划为固定提拨制(Defined Contribution),而受薪员工为固定受益制(Defined Benefit)。
现在,管理层决定办公室行政人员应该进入固定受益退休计划。但问题在于办公室行政人员是小时工,当前的模型并不支持。团队发现退休计划是由员工类型决定的,于是将模型重构如下:
只有超级管理员才能编辑 Employee Type 对象,而且只有公司策略变更时,他才能修改此对象。这些受限制的对象让开发人员想到了 KNOWLEDGE LEVEL 模式,他尝试按照这种思想,重新组织了一下模型:
受限制的对象都在 KNOWLEDGE LEVEL 中,可以自由编辑的对象都在 Operational Level 中,区分得非常清楚。由于模型变得更清晰了,开发人员发现 Employee Type 可以被指定为 Retirement Plan 中的任何一种, 也可以被指定为两种工资类型中的任何一种。这里实际上隐含了一个 Payroll 的概念,它和 Employee Type 混在了一起。于是开发人员重构了模型:
KNOWLEDGE LEVEL 是 REFLECTION(反射)模式在领域层中的一种应用。Java中有一些基本的反射机制,用于查询一个类的方法和属性等信息。
KNOWLEDGE LEVEL 看上去像是 RESPONSIBILITY LAYER 的一个特例,但事实并非如此。KNOWLEDGE LEVEL 中两个级别之间的依赖性是双向的,而在层次结构中,依赖是单向的。
模式: PLUGGABLE COMPONENT FRAMEWORK
在反复精炼后得到的成熟模型中,有机会使用 PLUGGABLE COMPONENT FRAMEWORK (可插入式组件框架)。
从接口和交互中提炼出一个 ABSTRACT CORE,并创建一个框架,这个框架需要允许这些接口的各种不同实现被自由替换。通用,无论是什么应用程序,只要它严格的通过 ABSTRACT CORE 的接口进行操作,那么就可以允许它使用这些组件。
小结
以下是各种大型结构和模型的关系:
结构越严格,约束越多,一致性就越高,设计也越容易理解。另一方面,约束会限制开发人员的灵活性,因此一定要克制,不要滥用框架和死板地实现大型结构。大型结构最重要的贡献在于它具有概念上的一致性,并帮助我们更深入地理解领域。每条结构规则都应该使开发变得更容易实现。
我们通常会将大型结构和 BOUNDED CONTEXT 结合起来使用。一种大型结构可以存在于一个 BOUNDED CONTEXT 中,也可以跨越多个 BOUNDED CONTEXT 存在,并用于组织 CONTEXT MAP。 前面的 RESPONSIBILITY LAYER 的例子被限定在一个 BOUNDED CONTEXT 中。这是解释这一思想的最简单的方法,也是该模式的一般用法。在这样的简单场景中,层名称的含义仅适用于该 CONTEXT,该 CONTEXT 中的模型元素或者子系统结构的名称也是如此。
制定战略设计决策时,需要注意以下要点:
- 决策必须传达到整个团队
- 决策过程必须收集反馈意见
- 计划必须允许演变
- 架构团队不必把所有最好、最聪明的人员都吸收进来
- 战略设计需要遵守简约和谦逊的原则
- 对象的职责要转移,而开发人员应该是多面手
结束语
在领域驱动方广为流行之前,很多项目的软件将创建得更快、更高效。但项目最终免不了按传统的套路发展,这导致先前精炼的深层模型无法被充分利用,更谈不上增强模型的能力了。如果做不到这一点,项目就无法在长达数年的时间内为用户提供稳定的价值。
我们很容易通过几个方面辨认出一个项目是否采用了领域驱动设计。标志性的特征是把“理解目标领域并将学到的知识融合到软件中”当作首要任务。其他工作都以它为前提。
在每个设计领域(如汽车、皮划艇或软件)中,我们都会把设计建立在已有的模式上,在已有主题范围内即兴发挥。有时我们必须发明一些全新的东西。但是,以标准的模式元素为基础,可以避免把精力浪费在那些已经有现成解决方案的问题上,从而集中精力关注我们的特殊问题。此外,根据传统的模式来简历自己的设计可以避免产生过于特殊的、很难交流的设计。
系列文章
- [DDD读书笔记] 运用模型①什么是领域模型
- [DDD读书笔记] 运用模型②通用语言
- [DDD读书笔记] 构造块①分离领域层
- [DDD读书笔记] 构造块②实体、值对象和服务
- [DDD读书笔记] 构造块③模块
- [DDD读书笔记] 构造块④聚合与工厂
- [DDD读书笔记] 构造块⑤仓库
- [DDD读书笔记] 构造块⑥实战模拟
- [DDD读书笔记] 重构①突破
- [DDD读书笔记] 重构②SPECIFICATION模式
- [DDD读书笔记] 重构③柔性设计
- [DDD读书笔记] 重构④使用分析模式和设计模式建模
- [DDD读书笔记] 战略设计①模型上下文策略
- [DDD读书笔记] 战略设计②精炼
- [DDD读书笔记] 战略设计③大型结构