《领域驱动设计:软件核心复杂性应对之道》书摘
作者:埃里克·埃文斯(Eric Evans)
# 第一部分 运用领域模型
原文:模型被用来描绘人们所关注的现实或想法的某个方面。模型是一种简化。它是对现实的解释——把与解决问题密切相关的方面抽象出来,而忽略无关的细节。
个人理解:模型是将事物的公共特性高度抽象出来,不要在乎细节。
原文:每个软件程序是为了执行用户的某项活动,或是满足用户的某种需求。这些用户应用软件的问题区域就是软件的领域。一些领域涉及物质世界,例如,机票预订程序的领域中包括飞机乘客在内。有些领域则是无形的,例如,会计程序的金融领域。软件领域一般与计算机关系不大,当然也有例外,例如,源代码控制系统的领域就是软件开发本身。
个人理解:程序软件服务于用户业务活动,解决业务活动中的问题就是一个领域,领域是多种多样的,并且不能仅仅依据业务类型来定义一个领域,领域可以是无形的,也可能独立于业务本身的。
原文:为了创建真正能为用户活动所用的软件,开发团队必须运用一整套与这些活动有关的知识体系。所需知识的广度可能令人望而生畏,庞大而复杂的信息也可能超乎想象。模型正是解决此类信息超载问题的工具。模型这种知识形式对知识进行了选择性的简化和有意的结构化。适当的模型可以使人理解信息的意义,并专注于问题。
个人理解:用户业务活动本身具有高度的复杂性和专业性,利用模型简化业务本身,便于业务理解以及聚焦问题的解决方案。
原文:领域模型并非某种特殊的图,而是这种图所要传达的思想。它绝不单单是领域专家头脑中的知识,而是对这类知识严格的组织且有选择的抽象。
个人理解:领域模型不是一个简单的模型,我认为应该是针对解决用户复杂且专业性的业务与业务活动中出现的问题的合理的组合抽象。
# 模型在领域驱动设计中的作用
原文:在领域驱动的设计中,3 个基本用途决定了模型的选择。
模型和设计的核心互相影响。正是模型与实现之间的紧密联系才使模型变得有用,并确保我们在模型中所进行的分析能够转化为最终产品(即一个可运行的程序)。模型与实现之间的这种紧密结合在维护和后续开发期间也会很有用,因为我们可以基于对模型的理解来解释代码。 模型是团队所有成员使用的通用语言的中枢。由于模型与实现之间的关联,开发人员可以使用该语言来讨论程序。他们可以在无需翻译的情况下与领域专家进行沟通。而且,由于该语言是基于模型的,因此我们可借助自然语言对模型本身进行精化。 模型是浓缩的知识。模型是团队一致认同的领域知识的组织方式和重要元素的区分方式。透过我们如何选择术语、分解概念以及将概念联系起来,模型记录了我们看待领域的方式。当开发人员和领域专家在将信息组织为模型时,这一共同的语言(模型)能够促使他们高效地协作。模型与实现之间的紧密结合使来自软件早期版本的经验可以作为反馈应用到建模过程中。
个人理解:领域驱动设计中的模型是抽象也是实现的依据,并且是开发团队的规范,是团队对于业务本身的统一理解。
# 软件的核心
原文:软件的核心是其为用户解决领域相关的问题的能力。所有其他特性,不管有多么重要,都要服务于这个基本目的。当领域很复杂时,这是一项艰巨的任务,要求高水平技术人员的共同努力。开发人员必须钻研领域以获取业务知识。他们必须磨砺其建模技巧,并精通领域设计。
个人理解:我们早期为了实现功能不要吹毛求疵的卡在这样实现优雅不优雅,这样性能高不高,软件仅仅依靠于业务本身,跳出业务去考虑的问题都可以往后放一放。并且很多程序员对于业务嗤之以鼻,其实你要是精通某个业务就不用敲代码了,而是作为某个业务专家提供技术顾问的角色,他不香吗?会写程序的干不过会写PPT的,这一句话也不是没有道理。😏
# 第 1 章 消化知识
作者着手设计一个用于设计印制电路板(PCB)的专用软件工具,讲了一大段故事和对话来说明自己的程序设计以及实现方法论。
# 有效建模的要素
原文:
模型和实现的绑定。 开发一个蕴含丰富知识的模型。 提炼模型。 头脑风暴和实验。
个人理解:对于模型作为团体中的共识,并且统一模型中术语,形成文档,不断更新文档保证文档的迭代和团体共识的更新,并且定期举行头脑风暴和验证,不断推翻重建模型,在不断的口述交谈中评判文档得优略,选择简介清楚的文档作为团体模型得最终版本。
# 知识消化
原文:高效的领域建模人员是知识的消化者。他们在大量信息中探寻有用的部分。他们不断尝试各种信息组织方式,努力寻找对大量信息有意义的简单视图。很多模型在尝试后被放弃或改造。只有找到一组适用于所有细节的抽象概念后,工作才算成功。这一精华严谨地表示了所发现的最为相关的知识。
个人理解:领域建模需要高度抽象。但是现在很多开发者获取到的消息是通过中间人获取到的(也就是所谓的产品经理),所以产品经理获取到的信息要足够精确才能保证建模的正确性。
原文:知识消化并非一项孤立的活动,它一般是在开发人员的领导下,由开发人员与领域专家组成的团队来共同协作。他们共同收集信息,并通过消化而将它组织为有用的形式。信息的原始资料来自领域专家头脑中的知识、现有系统的用户,以及技术团队以前在相关遗留系统或同领域的其他项目中积累的经验。信息的形式也多种多样,有可能是为项目编写的文档,有可能是业务中使用的文件,也有可能来自大量的讨论。
个人理解:领域建模需要结合业务人员,领域专家,以及历史系统经验等多方面的经验来做
# 持续学习
原文:高效率的团队需要有意识地积累知识,并持续学习。对于开发人员来说,这意味着既要完善技术知识,也要培养一般的领域建模技巧(如本书中所讲的那些技巧)。
个人理解:业务知识和技术知识同等重要,只有两者都兼备才能成为团队的中坚力量。
# 知识丰富的设计
原文:
业务活动和规则如同所涉及的实体一样,都是领域的核心,任何领域都有各种类别的概念。知识消化所产生的模型能够反映出对知识的深层理解。在模型发生改变的同时,开发人员对实现进行重构,以便反映出模型的变化,这样,新知识就被合并到应用程序中了。
当我们的建模不再局限于寻找实体和值对象时,我们才能充分吸取知识,因为业务规则之间可能会存在不一致。领域专家在反复研究所有规则、解决规则之间的矛盾以及以常识来弥补规则的不足等一系列工作中,往往不会意识到他们的思考过程有多么复杂。软件是无法完成这一工作的。正是通过与软件专家紧密协作来消化知识的过程才使得规则得以澄清和充实,并消除规则之间的矛盾以及删除一些无用规则。
个人理解:领域建模不仅仅是对业务发生对象实体和属性的抽象,而是对整个业务知识的了解,业务过程中的任何活动(也就是我们程序编码中的各个分叉业务逻辑)都是领域建模的核心内容,领域建模的抽象设计需要考虑程序设计的落地实现的可行性。
# 深层模型
原文:有用的模型很少停留在表面。随着对领域和应用程序需求的理解逐步加深,我们往往会丢弃那些最初看起来很重要的表面元素,或者切换它们的角度。这时,一些开始时不可能发现的巧妙抽象就会渐渐浮出水面,而它们恰恰切中问题的要害。
个人理解:领域建模不是一蹴而就,需要更具业务深层的了解不断优化不断迭代。
# 第 2 章 交流与语言的使用
原文:
领域模型可成为软件项目通用语言的核心。该模型是一组得自于项目人员头脑中的概念,以及反映了领域深层含义的术语和关系。这些术语和相互关系提供了模型语言的语义,虽然语言是为领域量身定制的,但就技术开发而言,其依然足够精确。正是这条至关重要的纽带,将模型与开发活动结合在一起,并使模型与代码紧密绑定。
# 通用语言(Ubiquitous Language )
原文:
Ubiquitous Language 是一种团队在开发过程中使用的统一语言:
- 由团队共同制定并理解,涵盖特定领域中的核心术语和概念。
- 集成于代码、文档、讨论中,使业务逻辑清晰一致。
# “大声地”建模
原文:改善模型的最佳方式之一就是通过对话来研究,试着大声说出可能的模型变化中的各种结构。这样不完善的地方很容易被听出来。
# 一个团队,一种语言
原文:有了 UBIQUITOUS LANGUAGE 之后,开发人员之间的对话、领域专家之间的讨论以及代码本身所表达的内容都基于同一种语言,都来自于一个共享的领域模型。
# 文档和图
原文:
当人们必须通过 UML 图表示整个模型或设计时,麻烦也随之而来。很多对象模型图在某些方面过于细致,同时在某些方面又有很多遗漏。说它们过于细致是因为人们认为必须将所有要编码的对象都放到建模工具中。而细节过多的结果是“只见树木,不见森林”。
尽管存在所有这些细节,但属性和关系只是对象模型的一部分。这些对象的行为以及这些对象上的约束就不那么容易表示了。对象交互图可以阐明设计中的一些复杂之处,但却无法用这种方式来展示大量的交互,就是工作量太大了,既要制作图,还要学习这些图。而且交互图也只能暗示出模型的目的。要想把约束和断言包括进来,需要在 UML 图中使用文本,这些文本用括号括起来,插入到图中。
个人理解:图做概要设计,文档要做细节设计,然后有机结合与补充。
# 书面设计文档
原文:
口头交流可以解释代码的含义,因此可作为代码精确性和细节的补充。虽然交谈对于将人们与模型联系起来是至关重要的,但书面文档也是必不可少的,任何规模的团队都需要它来提供稳定和共享的交流。但要想编写出能够帮助团队开发出好软件的书面文档却是一个不小的挑战。
一旦文档的形式变得一成不变,往往会从项目进展流程中脱离出来。它会跟不上代码或项目语言的演变。
评估文档的总体原则:
文档应作为代码和口头交流的补充
文档应当鲜活并保持最新
# Executable Bedrock
Executable Bedrock 通常是指一种确保领域模型与业务逻辑紧密联系、并且能够直接运行的核心代码基础。这一概念的核心思想是,模型不仅是抽象的文档或概念,而是能够转化为可执行代码,从而直接指导系统的设计和实现。
# 解释性模型
原文:
模型在帮助领域学习方面也具有很大价值。对设计起到推动作用的模型是领域的一个视图,但为了学习领域,还可以引入其他视图,这些视图只用作传递一般领域知识的教学工具。出于此目的,人们可以使用与软件设计无关的其他种类模型的图片或文字。
使用其他模型的一个特殊原因是范围。驱动软件开发过程的技术模型必须经过严格的精简,以便用最小化的模型来实现其功能。而解释性模型则可以包含那些提供上下文的领域方面—这些上下文用于澄清范围更窄的模型。
解释性模型不必是对象模型,而且最好不是。实际上在这些模型中不使用 UML 是有好处的,这样可以避免人们错误地认为这些模型与软件设计是一致的。尽管解释性模型与驱动设计的模型往往有对应关系,但它们并不完全类似。
个人理解:解释性模型对除了领域驱动模型的解释补充,并且最好与领域驱动模型分开,让人容易辨识。
# 第 3 章 绑定模型和实现
# MODEL-DRIVEN DESIGN
原文:
模型和程序设计之间的联系可能在很多情况下被破坏,但是二者的这种分离往往是有意而为之的。很多设计方法都提倡使用完全脱离于程序设计的分析模型,并且通常这二者是由不同的人员开发的。之所以称其为分析模型,是因为它是对业务领域进行分析的结果,它在组织业务领域中的概念时,完全不去考虑自己在软件系统中将会起到的作用。分析模型仅仅是理解工具,人们认为把它与程序实现联系在一起无异于搅浑一池清水。随后的程序设计与分析模型之间可能仅仅保持一种松散的对应关系。在创建分析模型时并没有考虑程序设计的问题,因此分析模型很有可能无法满足程序设计的需求。
MODEL-DRIVEN DESIGN(模型驱动设计)不再将分析模型和程序设计分离开,而是寻求一种能够满足这两方面需求的单一模型。不考虑纯粹的技术问题,程序设计中的每个对象都反映了模型中所描述的相应概念。这就要求我们以更高的标准来选择模型,因为它必须同时满足两种完全不同的目标。
个人理解:领域驱动模型是对整个系统维度的抽象,而对于太多的开发人员来说只负责整个系统的小模块,就很难以上帝视角来评判整个领域驱动模型,仅仅只能以分析模型来解决业务问题,开发人员认为自己的绝佳抽象其实成为了整个领域驱动模型知识的错误观点,从而导致整个系统的实现偏离,领域驱动模型的设计与程序设计需要组成一个单一模型,也就是可从领域驱动模型解读出程序设计的概念,不同开发人员对于领域驱动模型的理解,解读出不同的程序设计从大方向上不存在错误,但是也需要不断迭代纠偏,保持最终的领域驱动模型与设计的正确关联性。
# 建模范式和工具支持
原文:面向对象设计是目前大多数项目所使用的建模范式,也是本书中使用的主要方法。
# 揭示主旨:为什么模型对用户至关重要
原文:如果程序设计基于一个能够反映出用户和领域专家所关心的基本问题的模型,那么与其他设计方式相比,这种设计可以将其主旨更明确地展示给用户。让用户了解模型,将使他们有更多机会挖掘软件的潜能,也能使软件的行为合乎情理、前后一致。
# HANDS-ON MODELERS 模式
原文:
人们总是把软件开发比喻成制造业。这个比喻的一个推论是:经验丰富的工程师做设计工作,而技能水平较低的劳动力负责组装产品。这种做法使许多项目陷入困境,原因很简单——软件开发就是设计。虽然开发团队中的每个成员都有自己的职责,但是将分析、建模、设计和编程工作过度分离会对 MODEL-DRIVEN DESIGN 产生不良影响。
我曾经在一个项目中负责协调不同的应用程序开发团队,帮助开发可以驱动程序设计的领域模型。但是管理层认为建模人员就应该只负责建模工作,编写代码就是在浪费这种技能,于是他们不准我编写代码或者与程序员讨论细节问题。开始项目进展的还算顺利。我和领域专家以及各团队的开发负责人共同工作,消化领域知识并提炼出了一个不错的核心模型。但是该模型却从来没有派上用场,原因有两个。其一,模型的一些意图在其传递过程中丢失了。第二个原因是模型与程序实现及技术互相影响,而我无法直接获得这种反馈。
个人理解:HANDS-ON MODELER 是领域驱动设计和系统实现中的关键角色,注重理论与实践结合,能通过手动建模和快速验证推动系统设计与开发的成功。这一角色不仅需要深刻理解业务逻辑,还需要将其转化为高效的技术实现。
# 第二部分 模型驱动设计的构造块
# 第 4 章 分离领域
原文:在软件中,虽然专门用于解决领域问题的那部分通常只占整个软件系统的很小一部分,但其却出乎意料的重要。要想实现本书的想法,我们需要着眼于模型中的元素并且将它们视为一个系统。绝不能像在夜空中辨认星座一样,被迫从一大堆混杂的对象中将领域对象挑选出来。我们需要将领域对象与系统中的其他功能分离,这样就能够避免将领域概念和其他只与软件技术相关的概念搞混了,也不会在纷繁芜杂的系统中完全迷失了领域。
# LAYERED ARCHITECTURE
原文:
软件系统有各种各样的划分方式,但是根据软件行业的经验和惯例,普遍采用 LAYERED ARCHITECTURE(分层架构),特别是有几个层基本上已成了标准层。
LAYERED ARCHITECTURE 的基本原则是层中的任何元素都仅依赖于本层的其他元素或其下层的元素。向上的通信必须通过间接的方式进行。分层的价值在于每一层都只代表程序中的某一特定方面。这种限制使每个方面的设计都更具内聚性,更容易解释。当然,要分离出内聚设计中最重要的方面,选择恰当的分层方式是至关重要的。尽管 LAYERED ARCHITECTURE 的种类繁多,但是大多数成功的架构使用的都是下面这 4 个概念层的某种变体。
给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效地使用这些知识。领域层与基础设施层以及用户界面层分离,可以使每层的设计更加清晰。彼此独立的层更容易维护,因为它们往往以不同的速度发展并且满足不同的需求。层与层的分离也有助于在分布式系统中部署程序,不同的层可以灵活地放在不同服务器或者客户端中,这样可以减少通信开销,并优化程序性能。
个人理解:整个分层架构就是我们用DDD的最直观的感受,也就是所谓的VO,DTO,DO等分层对象,4 个概念层:也就是所谓的领域层,用户界面层、应用层以及基础设施层,分层的好处就是:高内聚低耦合,便于维护,便于拓展,便于在分布式中部署,减少通信开销,优化程序性能。
# 将各层关联起来
原文:
各层之间是松散连接的,层与层的依赖关系只能是单向的。上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的),以及采用常规的交互手段。而如果下层元素需要与上层元素进行通信(不只是回应直接查询),则需要采用另一种通信机制,使用架构模式来连接上下层,如回调模式或 OBSERVERS 模式。
最早将用户界面层与应用层和领域层相连的模式是 MODEL-VIEW-CONTROLLER(MVC,模型—视图—控制器)框架。
还有许多其他连接用户界面层和应用层的方式。对我们而言,只要连接方式能够维持领域层的独立性,保证在设计领域对象时不需要同时考虑可能与其交互的用户界面,那么这些连接方式就都是可用的。
通常,基础设施层不会发起领域层中的操作,它处于领域层“之下”,不包含其所服务的领域中的知识。事实上这种技术能力最常以 SERVICE 的形式提供。
应用层和领域层可以调用基础设施层所提供的 SERVICE。如果 SERVICE 的范围选择合理,接口设计完善,那么通过把详细行为封装到服务接口中,调用程序就可以保持与 SERVICE 的松散连接,并且自身也会很简单。
个人理解:以MVC框架来说明:
+-------------+ +-------------+ +-------------+
| Model |<------>| Controller |<------>| View |
+-------------+ +-------------+ +-------------+
↑ ↓ ↑
| | |
数据更新 用户输入处理 数据显示
2
3
4
5
6
7
view到controller再到model,上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的)。各层之间传递不同的数据对象,也就是也就是所谓的VO,DTO,DO等分层对象
# 架构框架
原文:架构框架和其他工具都在不断的发展。新框架将越来越多的应用技术问题变得自动化,或者为其提供了预先设定好的解决方案。如果框架使用得当,那么程序开发人员将可以更加专注于核心业务问题的建模工作,这会大大提高开发效率和程序质量。但与此同时,我们必须要保持克制,不要总是想着要寻找框架,因为精细的框架也可能会束缚住程序开发人员。
# 领域层是模型的精髓
原文:现在,大部分软件系统都采用了 LAYERED ARCHITECTURE,只是采用的分层方案存在不同而已。许多类型的开发工作都能从分层中受益。然而,领域驱动设计只需要一个特定的层存在即可。
领域模型是一系列概念的集合。“领域层”则是领域模型以及所有与其直接相关的设计元素的表现,它由业务逻辑的设计和实现组成。在 MODEL-DRIVEN DESIGN 中,领域层的软件构造反映出了模型概念。如果领域逻辑与程序中的其他关注点混在一起,就不可能实现这种一致性。将领域实现独立出来是领域驱动设计的前提。
个人理解:领域层(Domain Layer)是整个系统架构中负责表达核心业务逻辑和规则的部分,直接反映了领域专家和开发人员对业务需求的深刻理解和建模。
在分层架构中,领域层位于应用层之下,基础设施层和用户界面层之上:
+---------------------+ 用户交互
| Presentation Layer | <- UI 层
+---------------------+
| Application Layer | <- 应用层,协调业务流程
+---------------------+
| Domain Layer | <- 领域层,核心业务逻辑
+---------------------+
| Infrastructure Layer| <- 基础设施层,技术细节
+---------------------+
2
3
4
5
6
7
8
9
10
领域层专注于:
- 表达领域概念:通过领域模型(如实体、值对象、聚合等)清晰地表示业务问题。
- 定义业务规则:实现核心逻辑,确保业务规则得以正确执行。
- 维护状态一致性:通过模型之间的交互,保证领域内部状态的合理性。
# 领域层的核心组件
# 1. 实体(Entity)
- 特点:具有唯一标识(ID),生命周期由 ID 确定。
- 用途:表示领域中的核心业务对象。
- 示例:用户、订单、产品等。
public class User {
private Long id; // 唯一标识
private String name;
// 领域行为
public void changeName(String newName) {
this.name = newName;
}
}
2
3
4
5
6
7
8
9
# 2. 值对象(Value Object)
- 特点:无唯一标识,通过属性值确定相等性。
- 用途:表示一组不可变的属性,通常用于描述实体的特性。
- 示例:地址、货币、时间范围等。
public class Address {
private String street;
private String city;
// 不可变性
public Address(String street, String city) {
this.street = street;
this.city = city;
}
}
2
3
4
5
6
7
8
9
10
# 3. 聚合(Aggregate)和聚合根(Aggregate Root)
- 特点:聚合是一组领域对象的集合,聚合根是其访问入口。
- 用途:控制聚合内部的一致性,外界只能通过聚合根操作聚合内部对象。
- 示例:订单与订单项、购物车与商品。
public class Order {
private Long id;
private List<OrderItem> items;
// 添加订单项
public void addItem(OrderItem item) {
items.add(item);
}
}
2
3
4
5
6
7
8
9
# 4. 领域服务(Domain Service)
- 特点:当某些业务逻辑无法归属于单个实体或值对象时,用领域服务来表达。
- 用途:提供与领域相关的操作。
- 示例:支付服务、库存检查。
public class PaymentService {
public boolean processPayment(Order order) {
// 实现支付逻辑
return true;
}
}
2
3
4
5
6
# 5. 领域事件(Domain Event)
- 特点:表示领域中发生的重要事件。
- 用途:通知其他部分处理领域状态变化。
- 示例:用户注册成功、订单完成。
public class OrderPlacedEvent {
private Long orderId;
public OrderPlacedEvent(Long orderId) {
this.orderId = orderId;
}
}
2
3
4
5
6
7
# 领域层的关键特性
- 业务导向
- 领域层专注于业务逻辑,与技术实现无关。
- 代码应该贴近领域语言(Ubiquitous Language)。
- 高内聚,低耦合
- 保证聚合内的对象彼此协作,但聚合之间松散连接。
- 封装性
- 领域模型内部实现对外隐藏,外界只能通过明确的方法访问。
- 独立性
- 不依赖框架或基础设施层,便于测试和迁移。
# 领域层的核心价值
# 1. 反映业务本质
领域层直接映射领域知识,帮助开发团队和领域专家共同构建对业务的深刻理解。
# 2. 支撑复杂逻辑
通过模型、聚合和服务,领域层能够处理高度复杂的业务规则。
# 3. 实现可维护性
将业务逻辑从技术细节中抽离,使得代码更具可读性和可维护性。
# THE SMART UI“反模式”
原文:Smart UI "反模式" 指的是一种软件设计方式,其中用户界面(UI)层不仅负责呈现和交互,还包含了大量的业务逻辑和应用逻辑。这种设计方式虽然在简单系统中实现快速开发,但对于复杂应用会导致维护困难、测试繁琐和扩展受限。是分层模式的反面教材。
# 其他分离方式
原文:OUNDED CONTEXT 和 ANTICORRUPTION LAYER
个人理解:
在领域驱动设计(DDD,Domain-Driven Design)中,Bounded Context(界限上下文) 和 Anticorruption Layer(防腐层) 是两个重要的概念,它们帮助解决复杂系统中模型的边界和整合问题。
# 1. Bounded Context(界限上下文)
# 定义
Bounded Context 是指在领域驱动设计中明确划分的逻辑边界,它表示一个独立的领域模型及其行为。这一边界定义了模型在其中有效的范围,避免模型在不同上下文中产生歧义。
- 模型的局部性:同一个术语可能在不同上下文中有不同含义。
- 边界的清晰性:每个 Bounded Context 内的模型独立于其他上下文,拥有独立的业务逻辑和数据结构。
# 关键特性
- 独立性:每个上下文内有独立的领域模型,避免与其他上下文的混淆。
- 语义一致性:上下文内的模型语义是明确且统一的。
- 与业务紧密相关:上下文的划分通常依据业务领域的自然边界。
# 示例
假设一个电子商务平台包括以下业务:
- 用户管理(User Management)
- 订单管理(Order Management)
- 支付处理(Payment Processing)
每个模块可以是一个 Bounded Context:
- 用户在 用户管理上下文 中表示用户的注册信息。
- 用户在 订单管理上下文 中表示客户(Customer)。
- 用户在 支付处理上下文 中表示付款方(Payer)。
# 优点
- 降低耦合:各模块独立演进,减少跨模块依赖。
- 模型更清晰:上下文内模型含义明确。
- 代码可维护性高:更容易扩展和重构。
# 2. Anticorruption Layer(防腐层)
# 定义
Anticorruption Layer (ACL) 是指在两个系统或上下文之间创建的一个保护层,用来隔离和适配外部系统的模型或接口,防止外部系统的设计决策对本地系统产生“腐蚀性”影响。
# 作用
- 模型保护:本地上下文的领域模型不会被外部上下文或系统的不一致性影响。
- 适配外部模型:通过 ACL 转换外部上下文的模型或接口为本地上下文可理解的形式。
- 解耦:降低与外部系统的直接依赖,使本地系统的领域逻辑更专注。
# 关键机制
- 转换数据:从外部系统获取数据后,转换为本地上下文的模型。
- 屏蔽复杂性:封装外部系统的复杂性,使调用方感知不到外部的设计问题。
- 统一接口:为本地上下文提供一个一致的接口,即使外部系统发生变化也不会直接影响本地系统。
# 示例
假设电子商务平台需要与一个外部支付网关集成:
- 外部系统:支付网关返回的模型包含复杂的结构和冗余字段。
- 本地上下文:支付上下文需要统一的“支付订单”模型。
通过 ACL,适配器会将外部支付网关返回的数据转换为本地领域模型:
// 外部系统模型
class ExternalPaymentResponse {
String transactionId;
String status;
String gatewayMessage;
// 更多复杂字段
}
// 本地领域模型
class Payment {
String paymentId;
PaymentStatus paymentStatus;
String message;
}
// 防腐层
class PaymentAdapter {
public Payment adapt(ExternalPaymentResponse externalResponse) {
return new Payment(
externalResponse.transactionId,
mapStatus(externalResponse.status),
externalResponse.gatewayMessage
);
}
private PaymentStatus mapStatus(String externalStatus) {
// 将外部系统状态映射为本地状态
switch (externalStatus) {
case "SUCCESS":
return PaymentStatus.COMPLETED;
case "FAILED":
return PaymentStatus.FAILED;
default:
return PaymentStatus.UNKNOWN;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 优点
- 隔离复杂性:封装外部系统的实现细节,避免其直接影响领域逻辑。
- 提高灵活性:可以应对外部系统的变化。
- 减少技术债务:通过适配器模式,使领域层保持整洁。
# Bounded Context 与 Anticorruption Layer 的关系
- Bounded Context 强调明确领域模型的边界,并在不同上下文中保持独立的语义一致性。
- Anticorruption Layer 是 Bounded Context 的保护机制,防止外部上下文或系统的模型对本地上下文产生污染。
在实际应用中:
- 当两个 Bounded Context 需要交互时,可以使用 ACL 来适配模型。
- 如果需要与外部系统集成,也可以用 ACL 隔离领域逻辑与外部接口。
# 图示
+-----------------------+ +-----------------------+
| Bounded Context A | | Bounded Context B |
| (Local Model) | | (External Model) |
+-----------------------+ +-----------------------+
| |
| |
| +----------------+ |
+--> | ACL (Adapter) | <-----+
+----------------+
2
3
4
5
6
7
8
9
# 总结
- Bounded Context 是领域驱动设计中核心的边界划分工具,用来保持模型的语义一致性和独立性。
- Anticorruption Layer 是一种保护机制,通过适配和隔离外部系统,防止其影响本地上下文的领域模型。
两者结合使用,可以有效应对复杂系统中的模型边界和系统整合问题,从而提升系统的可维护性和稳定性。
# 第 5 章 软件中所表示的模型
原文:
要想在不削弱模型驱动设计能力的前提下对实现做出一些折中,需要重新组织基本元素。我们需要将模型与实现的各个细节一一联系起来。仔细考查具体模型选择与实现问题之间的关系,我们将着重区分用于表示模型的 3 种模型元素模式:ENTITY、VALUE OBJECT 和 SERVICE。
从表面上看,定义那些用来捕获领域概念的对象很容易,但要想反映其含义却很困难。这要求我们明确区分各种模型元素的含义,并与一系列设计实践结合起来,从而开发出特定类型的对象。
一个对象是用来表示某种具有连续性和标识的事物的呢(可以跟踪它所经历的不同状态,甚至可以跨不同的实现跟踪它),还是用于描述某种状态的属性呢?这是 ENTITY 与 VALUE OBJECT 之间的根本区别。明确地选择这两种模式中的一个来定义对象,有利于减少歧义,并帮助我们做出特定的选择,这样才能得到健壮的设计。
个人理解:
在领域驱动设计(DDD)中,ENTITY、VALUE OBJECT 和 SERVICE 是三种关键的模型元素,用于精确表达领域中的不同概念和职责。理解它们的特点和使用场景是 DDD 的基础。
# 1. ENTITY(实体)
# 定义
实体是具有唯一标识(ID)的模型元素,用于表示领域中的某种概念,其身份在系统中是持久且不可替代的。实体的状态可能会随着时间的推移而改变,但其身份始终不变。
# 特点
- 唯一标识:通过一个全局唯一的标识符(如 ID)区分不同的实体。
- 状态可变:实体的属性值可能会随着业务操作而发生变化。
- 生命周期:实体通常具有完整的生命周期管理。
# 使用场景
- 需要跟踪业务中概念的唯一性,例如用户、订单、车辆等。
- 实体的身份比其属性值更重要。
# 示例
在一个电子商务平台中:
- 用户(User) 是一个实体,其身份由其用户 ID 表示。
- 即使用户的姓名或邮箱发生变化,其身份仍然保持不变。
class User {
private String userId; // 唯一标识
private String name;
private String email;
// 构造函数
public User(String userId, String name, String email) {
this.userId = userId;
this.name = name;
this.email = email;
}
// Getter 和业务操作方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2. VALUE OBJECT(值对象)
# 定义
值对象是用于描述领域中的一种特定属性或特征的模型元素。它不具有唯一标识,其关注的是对象的值而非身份。值对象是不可变的(immutable)。
# 特点
- 无唯一标识:不同值对象只根据值来区分,而非身份。
- 不可变性:一旦创建,其值不能被改变(如果需要修改,则应创建一个新的实例)。
- 轻量级:通常用于描述领域中的属性或特定计算。
# 使用场景
- 表示领域中的属性,例如地址、坐标、货币等。
- 值的本质重要性高于身份。
# 示例
在一个物流系统中:
- 地址(Address) 是一个值对象,因为地址只关注值,而不关心身份。
class Address {
private String street;
private String city;
private String zipCode;
// 构造函数
public Address(String street, String city, String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
// Getter
public String getFullAddress() {
return street + ", " + city + " - " + zipCode;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3. SERVICE(服务)
# 定义
服务是表示领域中某些重要操作的模型元素,它们不能自然归属于单个实体或值对象,而是独立于具体对象的业务功能。服务定义的是行为,而非状态。
# 特点
- 无状态:服务本身通常是无状态的。
- 专注于操作:服务封装了复杂的业务逻辑或跨越多个实体的操作。
- 边界清晰:仅在无法归属于某个实体或值对象时才使用。
# 使用场景
- 某些操作涉及多个实体或值对象。
- 某些功能本质上不属于任何一个领域对象,例如支付、权限验证等。
# 示例
在一个电子商务平台中:
- 支付服务(PaymentService) 用于处理支付操作,这种逻辑涉及多个领域对象(用户、订单、支付网关等)。
class PaymentService {
public void processPayment(Order order, PaymentDetails paymentDetails) {
// 执行支付逻辑
if (paymentDetails.isValid()) {
order.setPaymentStatus("PAID");
} else {
throw new IllegalArgumentException("Invalid payment details");
}
}
}
2
3
4
5
6
7
8
9
10
# 三者的对比
模型元素 | 主要特征 | 是否需要 ID | 是否可变 | 关注点 | 示例 |
---|---|---|---|---|---|
ENTITY | 唯一身份,生命周期管理 | 是 | 是 | 身份 | 用户、订单、商品 |
VALUE OBJECT | 无身份,注重值且不可变 | 否 | 否 | 值的本质 | 地址、货币、时间范围 |
SERVICE | 表达复杂的行为或操作,不持有状态 | 否 | N/A | 操作 | 支付服务、库存检查服务 |
# 最佳实践
- 实体的设计要专注于唯一性:仅为需要跟踪身份的领域概念定义为实体。
- 值对象的设计要专注于简洁性和不可变性:使用值对象描述单一的、明确的领域属性。
- 服务的设计要专注于行为的逻辑性:确保服务的职责是明确且不可归属于单一实体或值对象的。
通过合理使用这三种模型元素,可以更清晰地表达领域模型中的概念,使代码更加可维护和易扩展。
# ASSOCIATIONS 关联
原文:
至少有 3 种方法可以使得关联更易于控制。
规定一个遍历方向。 添加一个限定符,以便有效地减少多重关联。 消除不必要的关联。
尽可能地对关系进行约束是非常重要的。双向关联意味着只有将这两个对象放在一起考虑才能理解它们。当应用程序不要求双向遍历时,可以指定一个遍历方向,以便减少相互依赖,并简化设计。理解了领域之后就可以自然地确定一个方向。像很多国家一样,美国有过很多位总统。这是一种双向的、一对多的关系。然而,在提到“乔治·华盛顿”这个名字时,我们很少会问“他是哪个国家的总统?”。从实用的角度讲,我们可以将这种关系简化为从国家到总统的单向关联。如图 5-1 所示。这种精化实际上反映了对领域的深入理解,而且也是一个更实用的设计。它表明一个方向的关联比另一个方向的关联更有意义且更重要。也使得 Person 类不受非基本概念 President 的束缚。
通常,通过更深入的理解可以得到一个“限定的”关系。进一步研究总统的例子就可以知道,一个国家在一段时期内只能有一位总统(内战期间或许有例外)。这个限定条件把多重关系简化为一对一关系,并且在模型中植入了一条明确的规则。如图 5-2 所示。1790 年谁是美国总统?乔治·华盛顿。限定多对多关联的遍历方向可以有效地将其实现简化为一对多关联,从而得到一个简单得多的设计。
当然,最终的简化是清除那些对当前工作或模型对象的基本含义来说不重要的关联。
个人理解:
在领域建模中如何处理**关联(Association)**的问题,以及通过约束和简化关联来提升模型的设计质量和可维护性。
1. 控制关联的三种方法
作者提出了三种控制关联的方法,目的是减少模型的复杂性:
规定一个遍历方向:
- 在关联中指定一个方向(单向)而非双向关联,减少相互依赖,从而简化设计。
- 例如:国家与总统的关系,通常只需要从国家指向总统,而不需要从总统指向国家。
添加限定符:
- 通过限定条件来减少关联的多重性,例如将“多对多”简化为“一对多”或“一对一”。
- 在总统的例子中,添加限定符(如时间段),明确每个时间段内一个国家只能有一位总统。
消除不必要的关联:
- 删除对当前领域模型无关紧要的关联,避免让模型过于复杂或冗余。
- 例如,如果模型的核心并不需要“总统反查国家”的功能,可以直接去掉这种双向关联。
2. 为什么需要对关联进行约束?
双向关联的复杂性:
双向关联使得两个对象的理解和使用都必须考虑对方,增加了相互依赖性。如果业务场景不需要双向遍历,单向关联能有效降低复杂性。对领域的深入理解:
深入分析业务需求后,往往会发现有些关联是可以弱化或删除的。关联的方向和约束应基于领域模型中的实际语义,而不是为了建模而建模。更实用的设计:
通过对关联进行约束,模型可以更准确地反映业务规则和实际需求,从而提升设计的清晰度和易用性。
# 总结
这段话强调了通过约束和简化关联来优化模型设计的思路:
- 明确方向:减少双向关联的使用。
- 限定条件:通过领域规则对关联的多重性进行优化。
- 清除冗余:删除不必要的关联。
这些方法反映了 DDD 的核心思想:通过深刻理解领域,将模型简化为仅反映关键业务逻辑和规则的形式,使模型更加清晰、高效和可维护。
# ENTITY(又称为 REFERENCE OBJECT)
可参考:第 5 章 软件中所表示的模型中的 ENTITY(实体)内容
# ENTITY 建模
原文:当对一个对象进行建模时,我们自然而然会考虑它的属性,而且考虑它的行为也显得非常重要。但 ENTITY 最基本的职责是确保连续性,以便使其行为更清楚且可预测。保持实体的简练是实现这一责任的关键。不要将注意力集中在属性或行为上,应该摆脱这些细枝末节,抓住 ENTITY 对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。此外,应该将行为和属性转移到与核心实体关联的其他对象中。这些对象中,有些可能是 ENTITY,有些可能是 VALUE OBJECT(这是本章接下来要讨论的模式)。除了标识问题之外,实体往往通过协调其关联对象的操作来完成自己的职责
个人理解:ENTITY 的建模关键在于:
- 聚焦身份:身份是实体的核心,确保其唯一性和连续性。
- 保持简练:只关注领域内至关重要的行为和属性。
- 合理分配职责:将非核心职责转移给其他 ENTITY 或 VALUE OBJECT。
- 管理关联对象:通过协调关联对象完成更复杂的领域操作。
这样设计出来的 ENTITY 会更具可维护性、更贴合领域逻辑,同时避免了模型的冗余和复杂化。
# 设计标识操作
原文:每个 ENTITY 都必须有一种建立标识的操作方式,以便与其他对象区分开,即使这些对象与它具有相同的描述属性。不管系统是如何定义的,都必须确保标识属性在系统中是唯一的,即使是在分布式系统中,或者对象已被归档,也必须确保标识的唯一性。
个人理解:ENTITY 需要一个唯一标识。
# VALUE OBJECT
原文:
用于描述领域的某个方面而本身没有概念标识的对象称为 VALUE OBJECT(值对象)。VALUE OBJECT 被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。
VALUE OBJECT 经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。VALUE OBJECT 可以用作 ENTITY(以及其他 VALUE)的属性。我们可以把一个人建模为一个具有标识的 ENTITY,但这个人的名字是一个 VALUE。
当我们只关心一个模型元素的属性时,应把它归类为 VALUE OBJECT。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。VALUE OBJECT 应该是不可变的。不要为它分配任何标识,而且不要把它设计成像 ENTITY 那么复杂。
个人理解:VALUE OBJECT是一个ENTITY的属性,多个VALUE OBJECT可以组成一个独立的ENTITY。
# 设计 VALUE OBJECT
原文:我们并不关心使用的是 VALUE OBJECT 的哪个实例。由于不受这方面的约束,设计可以获得更大的自由,因此可以简化设计或优化性能。在设计 VALUE OBJECT 时有多种选择,包括复制、共享或保持 VALUE OBJECT 不变。
复制和共享哪个更划算取决于实现环境。虽然复制有可能导致系统被大量的对象阻塞,但共享可能会减慢分布式系统的速度。当在两个机器之间传递一个副本时,只需发送一条消息,而且副本到达接收端后是独立存在的。但如果共享一个实例,那么只会传递一个引用,这要求每次交互都要向发送方返回一条消息。
以下几种情况最好使用共享,这样可以发挥共享的最大价值并最大限度地减少麻烦:
节省数据库空间或减少对象数量是一个关键要求时; 通信开销很低时(如在中央服务器中);
共享的对象被严格限定为不可变时。
保持 VALUE OBJECT 不变可以极大地简化实现,并确保共享和引用传递的安全性。而且这样做也符合值的意义。如果属性的值发生改变,我们应该使用一个不同的 VALUE OBJECT,而不是修改现有的 VALUE OBJECT。尽管如此,在有些情况下出于性能考虑,仍需要让 VALUE OBJECT 是可变的。这包括以下因素:
如果 VALUE 频繁改变; 如果创建或删除对象的开销很大; 如果替换(而不是修改)将打乱集群(像前面示例中讨论的那样); 如果 VALUE 的共享不多,或者共享不会提高集群性能,或其他某种技术原因。
个人理解:设计 VALUE OBJECT(值对象) 时的关键选择和权衡,核心观点如下:
- 设计自由与性能优化:
- 因为 VALUE OBJECT 没有唯一身份,不受实例约束,可以灵活选择设计方式(复制、共享或不可变性)。
- 复制:减少通信复杂性,适合分布式系统,但可能增加对象数量。
- 共享:节约资源,适合集中系统,但增加分布式系统的通信开销。
- 共享适用场景:
- 需要节省数据库空间或减少对象数量时。
- 通信开销较低(如在中央服务器中)。
- 共享对象是 不可变 时,能保证数据安全和一致性。
- 不可变性优势:
- 符合值对象的本质属性。
- 简化实现,避免共享和引用传递中的数据修改风险。
- 可变性场景: 在某些情况下出于性能考虑,可以允许 VALUE OBJECT 可变:
- 值频繁改变。
- 创建或删除对象的开销较高。
- 替换对象可能破坏集群设计。
- 共享带来的性能提升不显著或技术上不可行。
# 设计包含 VALUE OBJECT 的关联
原文:
关联有关的大部分内容也适用于 ENTITY 和 VALUE OBJECT。模型中的关联越少越好,越简单越好。
ENTITY 和 VALUE OBJECT 是传统对象模型的主要元素,但一些注重实效的设计人员正逐渐开始使用一种新的元素——SERVICE。
# SERVICE
原文:
在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是 SERVICE(服务)。
SERVICE 是作为接口提供的一种操作,它在模型中是独立的,它不像 ENTITY 和 VALUE OBJECT 那样具有封装的状态。SERVICE 是技术框架中的一种常见模式,但它们也可以在领域层中使用。
所谓 SERVICE,它强调的是与其他对象的关系。与 ENTITY 和 VALUE OBJECT 不同,它只是定义了能够为客户做什么。SERVICE 往往是以一个活动来命名,而不是以一个 ENTITY 来命名,也就是说,它是动词而不是名词。SERVICE 也可以有抽象而有意义的定义,只是它使用了一种与对象不同的定义风格。SERVICE 也应该有定义的职责,而且这种职责以及履行它的接口也应该作为领域模型的一部分来加以定义。
使用 SERVICE 时应谨慎,它们不应该替代 ENTITY 和 VALUE OBJECT 的所有行为。但是,当一个操作实际上是一个重要的领域概念时,SERVICE 很自然就会成为 MODEL-DRIVEN DESIGN 中的一部分。将模型中的独立操作声明为一个 SERVICE,而不是声明为一个不代表任何事情的虚拟对象,可以避免对任何人产生误导。
好的 SERVICE 有以下 3 个特征。
与领域概念相关的操作不是 ENTITY 或 VALUE OBJECT 的一个自然组成部分。 接口是根据领域模型的其他元素定义的。 操作是无状态的
# SERVICE 与孤立的领域层
原文:
这种模式只重视那些在领域中具有重要意义的 SERVICE,但 SERVICE 并不只是在领域层中使用。我们需要注意区分属于领域层的 SERVICE 和那些属于其他层的 SERVICE,并划分责任,以便将它们明确地区分开。
文献中所讨论的大多数 SERVICE 是纯技术的 SERVICE,它们都属于基础设施层。领域层和应用层的 SERVICE 与这些基础设施层 SERVICE 进行协作。
很多领域或应用层 SERVICE 是在 ENTITY 和 VALUE OBJECT 的基础上建立起来的,它们的行为类似于将领域的一些潜在功能组织起来以执行某种任务的脚本。ENTITY 和 VALUE OBJECT 往往由于粒度过细而无法提供对领域层功能的便捷访问。我们在这里会遇到领域层与应用层之间很微妙的分界线。
在大多数开发系统中,在一个领域对象和外部资源之间直接建立一个接口是很别扭的。我们可以利用一个 FACADE(外观)将这样的外部 SERVICE 包装起来,这个外观可能以模型作为输入。
个人理解:SERVICE 模式 的本质及其在不同层中的作用,核心理解如下:
SERVICE 的分类与职责划分:
- 领域层 SERVICE:聚焦领域逻辑,基于 ENTITY 和 VALUE OBJECT,组织复杂任务或功能。
- 应用层 SERVICE:处理跨领域的操作,协调领域层功能,提供更高层次的服务。
- 基础设施层 SERVICE:提供技术支持(如数据库、文件系统),不包含领域逻辑。
领域层 SERVICE 的意义:
- ENTITY 和 VALUE OBJECT 过于细粒度,难以直接提供高层功能,领域层 SERVICE 将其行为组织成可用的功能单元。
- 领域层 SERVICE 通过封装复杂逻辑,提供便捷访问领域功能的方式。
FACADE 的作用:
- 解决领域对象与外部资源直接交互的复杂性。
- 用外观模式包装外部 SERVICE,以模型为接口,简化领域层与基础设施的协作。
# 粒度
原文:
SERVICE 的讨论强调的是将一个概念建模为 SERVICE 的表现力,但 SERVICE 还有其他有用的功能,它可以控制领域层中的接口的粒度,并且避免客户端与 ENTITY 和 VALUE OBJECT 耦合。
如前所述,由于应用层负责对领域对象的行为进行协调,因此细粒度的领域对象可能会把领域层的知识泄漏到应用层中。这产生的结果是应用层不得不处理复杂的、细致的交互,从而使得领域知识蔓延到应用层或用户界面代码当中,而领域层会丢失这些知识。明智地引入领域层服务有助于在应用层和领域层之间保持一条明确的界限。
个人理解:在各个独立的基础设施层的service完成复杂的业务交织时,提出一个独立的service作为组合各个独立service的聚合,做到高内聚低耦合。我一般是提出一个facade作为各个服务调用的入口,从名称就能快速区分service的类别。
# 对 SERVICE 的访问
# MODULE(也称为 PACKAGE)
原文:
MODULE 是一个传统的、较成熟的设计元素。虽然使用模块有一些技术上的原因,但主要原因却是“认知超载”。MODULE 为人们提供了两种观察模型的方式,一是可以在 MODULE 中查看细节,而不会被整个模型淹没,二是观察 MODULE 之间的关系,而不考虑其内部细节。
领域层中的 MODULE 应该成为模型中有意义的部分,MODULE 从更大的角度描述了领域。
像领域驱动设计中的其他元素一样,MODULE 是一种表达机制。MODULE 的选择应该取决于被划分到模块中的对象的意义。当你将一些类放到 MODULE 中时,相当于告诉下一位看到你的设计的开发人员要把这些类放在一起考虑。如果说模型讲述了一个故事,那么 MODULE 就是这个故事的各个章节。模块的名称表达了其意义。
个人理解:大家都知道分模块开发,但是其实大多数人都无法准确的再领域驱动设计中很好的分模块,并且忽略模块在领域驱动中的作用,我接触下来的很多公司仅仅是以功能划分模块,如何做好模块划分:
- 基于领域意义:
- 根据领域逻辑,将高度相关的对象和行为归类到同一模块。
- 模块名称应能清晰表达其在领域中的角色和意义。
- 降低耦合,增强内聚:
- 模块内部对象关系紧密,模块之间依赖尽量减少。
- 确保模块能单独理解并实现其领域功能。
- 关注模块粒度:
- 模块应具有足够的细粒度以体现语义单元,但不宜过度分割,避免复杂性增加。
- 强调业务边界:
- 模块划分应与领域边界一致(如 BOUNDED CONTEXT)。
- 清晰定义模块的责任和与其他模块的协作方式。
- 与团队协作一致:
- 模块划分能直观传递设计意图,方便团队成员理解和维护。
# 敏捷的 MODULE
原文:MODULE 需要与模型的其他部分一同演变。这意味着 MODULE 的重构必须与模型和代码一起进行。但这种重构通常不会发生。更改 MODULE 可能需要大范围地更新代码。这些更改可能会对团队沟通起到破坏作用,甚至会妨碍开发工具(如源代码控制系统)的使用。
# 通过基础设施打包时存在的隐患
原文:
精巧的技术打包方案会产生如下两个代价。
如果框架的分层惯例把实现概念对象的元素分得很零散,那么代码将无法再清楚地表示模型。 人的大脑把划分后的东西还原成原样的能力是有限的,如果框架把人的这种能力都耗尽了,那么领域开发人员就无法再把模型还原成有意义的部分了。
最好把事情变简单。要极度简化技术分层规则,要么这些规则对技术环境特别重要,要么这些规则真正有助于开发。例如,将复杂的数据持久化代码从对象的行为方面提取出来可以使重构变得更简单。
除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码放在同一个模块中(如果不能放在同一个对象中的话)。
从传统的“高内聚、低耦合”标准也可以得出相同的结论。实现业务逻辑的对象与负责数据库访问的对象之间的联系非常广泛,因此它们之间的耦合度很高。
领域模型中的每个概念都应该在实现元素中反映出来。ENTITY、VALUE OBJECT、它们之间的关联、领域 SERVICE 以及用于组织元素的 MODULE 都是实现与模型直接对应的地方。实现中的对象、指针和检索机制必须直接、清楚地映射到模型元素。如果没有做到这一点,就要重写代码,或者回头修改模型,或者同时修改代码和模型。
个人理解:
- 技术打包的隐患:
- 概念对象分散:框架若将实现元素分得过于零散,会使代码无法清晰反映领域模型。
- 认知超载:复杂的分层和技术规则耗尽开发者的理解力,难以还原模型的语义。
- 应对策略:
- 简化分层规则:只保留必要且有实际价值的技术规则,避免过度复杂化。
- 减少分布式设计:除非必须分布到多服务器,否则将实现同一概念对象的代码集中于一个模块内。
- 设计原则:
- 高内聚、低耦合:确保业务逻辑和数据库访问对象紧密协作,但避免耦合过高。
- 模型直接映射:领域模型的概念(ENTITY、VALUE OBJECT、SERVICE、MODULE)必须在实现中清晰体现,代码结构应直观对应模型。
- 调整建议:
- 如果实现不能直接反映模型,需重写代码或修改模型,以确保一致性。
# 建模范式
原文:MODEL-DRIVEN DESIGN 要求使用一种与建模范式协调的实现技术。人们曾经尝试了大量的建模范式,但在实践中只有少数几种得到了广泛应用。目前,主流的范式是面向对象设计,而且现在的大部分复杂项目都开始使用对象。这种范式的流行有许多原因,包括对象本身的固有因素、一些环境因素,以及广泛使用所带来的一些优势。
个人理解:主流的范式是面向对象设计
# 对象范式流行的原因
原文:
一些团队选择对象范式并不是出于技术上的原因,甚至也不是出于对象本身的原因,而是从一开始,对象建模就在简单性和复杂性之间实现了一个很好的平衡。
这就是目前大部分采用 MODEL-DRIVEN DESIGN 的项目很明智地使用面向对象技术作为系统核心的原因。它们不会被束缚在只有对象的系统里,因为对象已经成为内业的主流技术,人们目前使用的几乎所有的技术都有与之对应的集成工具。
个人理解:面向对象建模简单易用,且行业通识性广。
# 对象世界中的非对象
原文:
领域模型不一定是对象模型。
不管在项目中使用哪种主要的模型范式,领域中都会有一些部分更容易用某种其他范式来表达。当领域中只有个别元素适合用其他范式时,开发人员可以接受一些蹩脚的对象,以使整个模型保持一致(或者,在另一种极端的情况下,如果大部分问题领域都更适合用其他范式来表达,那么可以整个改为使用那种范式,并选择一个不同的实现平台)。但是,当领域的主要部分明显属于不同的范式时,明智的做法是用适合各个部分的范式对其建模,并使用混合工具集来进行实现。
# 在混合范式中坚持使用 MODEL-DRIVEN DESIGN
原文:
当将非对象元素混合到以面向对象为主的系统中时,需要遵循以下 4 条经验规则。
不要和实现范式对抗。我们总是可以用别的方式来考虑领域。找到适合于范式的模型概念。 把通用语言作为依靠的基础。即使工具之间没有严格联系时,语言使用上的高度一致性也能防止各个设计部分分裂。 不要一味依赖 UML。有时固定使用某种工具(如 UML 绘图工具)将导致人们通过歪曲模型来使它更容易画出来。例如,UML 确实有一些特性很适合表达约束,但它并不是在所有情况下都适用。有时使用其他风格的图形(可能适用于其他范式)或者简单的语言描述比牵强附会地适应某种对象视图更好。 保持怀疑态度。工具是否真正有用武之地?不能因为存在一些规则,就必须使用规则引擎。规则也可以表示为对象,虽然可能不是特别优雅。多个范式会使问题变得非常复杂。
# 第 6 章 领域对象的生命周期
原文:
有些对象具有更长的生命周期,其中一部分时间不是在活动内存中度过的。它们与其他对象具有复杂的相互依赖性。它们会经历一些状态变化,在变化时要遵守一些固定规则。管理这些对象时面临诸多挑战,稍有不慎就会偏离 MODEL-DRIVEN DESIGN 的轨道。
主要的挑战有以下两类。
在整个生命周期中维护完整性。 防止模型陷入管理生命周期复杂性造成的困境当中。
将通过 3 种模式解决这些问题。
首先是 AGGREGATE(聚合),它通过定义清晰的所属关系和边界,并避免混乱、错综复杂的对象关系网来实现模型的内聚。聚合模式对于维护生命周期各个阶段的完整性具有至关重要的作用。
我们将注意力转移到生命周期的开始阶段,使用 FACTORY(工厂)来创建和重建复杂对象和 AGGREGATE(聚合),从而封装它们的内部结构。最后,在生命周期的中间和末尾使用 REPOSITORY(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。
尽管 REPOSITORY 和 FACTORY 本身并不是来源于领域,但它们在领域设计中扮演着重要的角色。这些结构提供了易于掌握的模型对象处理方式,使 MODEL-DRIVEN DESIGN 更完备。
使用 AGGREGATE 进行建模,并且在设计中结合使用 FACTORY 和 REPOSITORY,这样我们就能够在模型对象的整个生命周期中,以有意义的单元、系统地操纵它们。AGGREGATE 可以划分出一个范围,这个范围内的模型元素在生命周期各个阶段都应该维护其固定规则。FACTORY 和 REPOSITORY 在 AGGREGATE 基础上进行操作,将特定生命周期转换的复杂性封装起来。
个人理解:
领域驱动设计管理领域对象的生命周期的方式有三种:
- AGGREGATE(聚合)组合重建复杂对象
- FACTORY(工厂)来创建和对象
- REPOSITORY(存储库)查找和检索持久化对象并封装庞大基础设施
# AGGREGATE
原文:尽管从表面上看这个问题是数据库事务方面的一个技术难题,但它的根源却在模型,归根结底是由于模型中缺乏明确定义的边界。从模型得到的解决方案将使模型更易于理解,并且使设计更易于沟通。当模型被修改时,它将引导我们对实现做出修改。
首先,我们需要用一个抽象来封装模型中的引用。AGGREGATE 就是一组相关对象的集合,我们把它作为数据修改的单元。每个 AGGREGATE 都有一个根(root)和一个边界(boundary)。边界定义了 AGGREGATE 的内部都有什么。根则是 AGGREGATE 所包含的一个特定 ENTITY。对 AGGREGATE 而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。除根以外的其他 ENTITY 都有本地标识,但这些标识只在 AGGREGATE 内部才需要加以区别,因为外部对象除了根 ENTITY 之外看不到其他对象。
固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及 AGGREGATE 成员之间的内部关系。而任何跨越 AGGREGATE 的规则将不要求每时每刻都保持最新状态。通过事件处理、批处理或其他更新机制,这些依赖会在一定的时间内得以解决。
为了实现这个概念上的 AGGREGATE,需要对所有事务应用一组规则。
根 ENTITY 具有全局标识,它最终负责检查固定规则。 根 ENTITY 具有全局标识。边界内的 ENTITY 具有本地标识,这些标识只在 AGGREGATE 内部才是唯一的。 AGGREGATE 外部的对象不能引用除根 ENTITY 之外的任何内部对象。根 ENTITY 可以把对内部 ENTITY 的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个 VALUE OBJECT 的副本传递给另一个对象,而不必关心它发生什么变化,因为它只是一个 VALUE,不再与 AGGREGATE 有任何关联。 作为上一条规则的推论,只有 AGGREGATE 的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。 AGGREGATE 内部的对象可以保持对其他 AGGREGATE 根的引用。 删除操作必须一次删除 AGGREGATE 边界之内的所有对象。(利用垃圾收集机制,这很容易做到。由于除根以外的其他对象都没有外部引用,因此删除了根以后,其他对象均会被回收。) 当提交对 AGGREGATE 边界内部的任何对象的修改时,整个 AGGREGATE 的所有固定规则都必须被满足。
我们应该将 ENTITY 和 VALUE OBJECT 分门别类地聚集到 AGGREGATE 中,并定义每个 AGGREGATE 的边界。在每个 AGGREGATE 中,选择一个 ENTITY 作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保 AGGREGATE 中的对象满足所有固定规则,也可以确保在任何状态变化时 AGGREGATE 作为一个整体满足固定规则。
个人理解:
# FACTORY
原文:
当创建一个对象或创建整个 AGGREGATE 时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用 FACTORY 进行封装。
对象的功能主要体现在其复杂的内部配置以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全剔除为止。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。
每种面向对象的语言都提供了一种创建对象的机制(例如,Java 和 C++中的构造函数,Smalltalk 中创建实例的类方法),但我们仍然需要一种更加抽象且不与其他对象发生耦合的构造机制。这就是 FACTORY,它是一种负责创建其他对象的程序元素。
应该将创建复杂对象的实例和 AGGREGATE 的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建 AGGREGATE 时要把它作为一个整体,并确保它满足固定规则。
任何好的工厂都需满足以下两个基本需求。
每个创建方法都是原子的,而且要保证被创建对象或 AGGREGATE 的所有固定规则。FACTORY 生成的对象要处于一致的状态。在生成 ENTITY 时,这意味着创建满足所有固定规则的整个 AGGREGATE,但在创建完成后可以向聚合添加可选元素。在创建不变的 VALUE OBJECT 时,这意味着所有属性必须被初始化为正确的最终状态。如果 FACTORY 通过其接口收到了一个创建对象的请求,而它又无法正确地创建出这个对象,那么它应该抛出一个异常,或者采用其他机制,以确保不会返回错误的值。 FACTORY 应该被抽象为所需的类型,而不是所要创建的具体类。
# 选择 FACTORY 及其应用位置
原文:
FACTORY 与被构建对象之间是紧密耦合的,因此 FACTORY 应该只被关联到与被构建对象有着密切联系的对象上。当有些细节需要隐藏(无论要隐藏的是具体实现还是构造的复杂性)而又找不到合适的地方来隐藏它们时,必须创建一个专用的 FACTORY 对象或 SERVICE。整个 AGGREGATE 通常由一个独立的 FACTORY 来创建,FACTORY 负责把对根的引用传递出去,并确保创建出的 AGGREGATE 满足固定规则。如果 AGGREGATE 内部的某个对象需要一个 FACTORY,而这个 FACTORY 又不适合在 AGGREGATE 根上创建,那么应该构建一个独立的 FACTORY。但仍应遵守规则——把访问限制在 AGGREGATE 内部,并确保从 AGGREGATE 外部只能对被构建对象进行临时引用。
# 有些情况下只需使用构造函数
原文:
我曾经在很多代码中看到所有实例都是通过直接调用类构造函数来创建的,或者是使用编程语言的最基本的实例创建方式。FACTORY 的引入提供了巨大的优势,而这种优势往往并未得到充分利用。但是,在有些情况下直接使用构造函数确实是最佳选择。FACTORY 实际上会使那些不具有多态性的简单对象复杂化。
在以下情况下最好使用简单的、公共的构造函数。
类(class)是一种类型(type)。它不是任何相关层次结构的一部分,而且也没有通过接口实现多态性。 客户关心的是实现,可能是将其作为选择 STRATEGY 的一种方式。 客户可以访问对象的所有属性,因此向客户公开的构造函数中没有嵌套的对象创建。 构造并不复杂。 公共构造函数必须遵守与 FACTORY 相同的规则:它必须是原子操作,而且要满足被创建对象的所有固定规则。
不要在构造函数中调用其他类的构造函数。构造函数应该保持绝对简单。复杂的装配,特别是 AGGREGATE,需要使用 FACTORY。使用 FACTORY METHOD 的门槛并不高。
# 接口的设计
原文:
当设计 FACTORY 的方法签名时,无论是独立的 FACTORY 还是 FACTORY METHOD,都要记住以下两点
每个操作都必须是原子的。我们必须在与 FACTORY 的一次交互中把创建对象所需的所有信息传递给 FACTORY。同时必须确定当创建失败时将执行什么操作,比如某些固定规则没有被满足。可以抛出一个异常或仅仅返回 null。为了保持一致,可以考虑采用编码标准来处理所有 FACTORY 的失败。 Factory 将与其参数发生耦合。如果在选择输入参数时不小心,可能会产生错综复杂的依赖关系。耦合程度取决于对参数(argument)的处理。如果只是简单地将参数插入到要构建的对象中,则依赖度是适中的。如果从参数中选出一部分在构造对象时使用,耦合将更紧密。
最安全的参数是那些来自较低设计层的参数。即使在同一层中,也有一种自然的分层倾向,其中更基本的对象被更高层的对象使用
使用抽象类型的参数,而不是它们的具体类。FACTORY 与被构建对象的具体类发生耦合,而无需与具体的参数发生耦合。
个人理解:
# 固定规则的相关逻辑应放置在哪里
原文:
FACTORY 负责确保它所创建的对象或 AGGREGATE 满足所有固定规则,然而在把应用于一个对象的规则移到该对象外部之前应三思。FACTORY 可以将固定规则的检查工作委派给被创建对象,而且这通常是最佳选择。
但 FACTORY 与被创建对象之间存在一种特殊关系。FACTORY 已经知道被创建对象的内部结构,而且创建 FACTORY 的目的与被创建对象的实现有着密切的联系。在某些情况下,把固定规则的相关逻辑放到 FACTORY 中是有好处的,这样可以让被创建对象的职责更明晰。对于 AGGREGATE 规则来说尤其如此(这些规则会约束很多对象)。但固定规则的相关逻辑却特别不适合放到那些与其他领域对象关联的 FACTORY METHOD 中。
个人理解:
# ENTITY FACTORY 与 VALUE OBJECT FACTORY
原文:
ENTITY FACTORY 与 VALUE OBJECT FACTORY 有两个方面的不同。由于 VALUE OBJECT 是不可变的,因此,FACTORY 所生成的对象就是最终形式。因此 FACTORY 操作必须得到被创建对象的完整描述。而 ENTITY FACTORY 则只需具有构造有效 AGGREGATE 所需的那些属性。对于固定规则不关心的细节,可以之后再添加。
个人理解:
# 重建已存储的对象
原文:
到目前为止,FACTORY 只是发挥了它在对象生命周期开始时的作用。到了某一时刻,大部分对象都要存储在数据库中或通过网络传输,而在当前的数据库技术中,几乎没有哪种技术能够保持对象的内容特征。大多数传输方法都要将对象转换为平面数据才能传输,这使得对象只能以非常有限的形式出现。因此,检索操作潜在地需要一个复杂的过程将各个部分重新装配成一个可用的对象。
用于重建对象的 FACTORY 与用于创建对象的 FACTORY 很类似,主要有以下两点不同。
用于重建对象的 ENTITY FACTORY 不分配新的跟踪 ID。如果重新分配 ID,将丢失与先前对象的连续性。因此,在重建对象的 FACTORY 中,标识属性必须是输入参数的一部分。 当固定规则未被满足时,重建对象的 FACTORY 采用不同的方式进行处理。当创建新对象时,如果未满足固定规则,FACTORY 应该简单地拒绝创建对象,但在重建对象时则需要更灵活的响应。如果对象已经在系统的某个地方存在(如在数据库中),那么不能忽略这个事实。但是,同样也不能任凭规则被破坏。必须通过某种策略来修复这种不一致的情况,这使得重建对象比创建新对象更困难。
FACTORY 封装了对象创建和重建时的生命周期转换。还有一种转换大大增加了领域设计的技术复杂性,这是对象与存储之间的互相转换。这种转换由另一种领域设计构造来处理,它就是 REPOSITORY
个人理解:
# REPOSITORY
原文:
领域驱动设计的目标是通过关注领域模型(而不是技术)来创建更好的软件。假设开发人员构造了一个 SQL 查询,并将它传递给基础设施层中的某个查询服务,然后再根据得到的表行数据的结果集提取出所需信息,最后将这些信息传递给构造函数或 FACTORY。开发人员执行这一连串操作的时候,早已不再把模型当作重点了。我们很自然地会把对象看作容器来放置查询出来的数据,这样整个设计就转向了数据处理风格。虽然具体的技术细节有所不同,但问题仍然存在——客户处理的是技术,而不是模型概念。诸如 METADATA MAPPING LAYER[Fowler 2002]这样的基础设施可以提供很大帮助,利用它很容易将查询结果转换为对象,但开发人员考虑的仍然是技术机制,而不是领域。更糟的是,当客户代码直接使用数据库时,开发人员会试图绕过模型的功能(如 AGGREGATE,甚至是对象封装),而直接获取和操作他们所需的数据。这将导致越来越多的领域规则被嵌入到查询代码中,或者干脆丢失了。虽然对象数据库消除了转换问题,但搜索机制还是很机械的,开发人员仍倾向于要什么就去拿什么。
客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过 AGGREGATE 的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而 ENTITY 和 VALUE OBJECT 则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。
REPOSITORY 将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合(collection),只是具有更复杂的查询功能。在添加或删除相应类型的对象时,REPOSITORY 的后台机制负责将对象添加到数据库中,或从数据库中删除对象。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对 AGGREGATE 根的整个生命周期的全程访问。
为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的 AGGREGATE 根提供 REPOSITORY。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给 REPOSITORY 来完成。
# REPOSITORY 的查询
# 客户代码可以忽略 REPOSITORY 的实现,但开发人员不能忽略
# REPOSITORY 的实现
# 在框架内工作
# REPOSITORY 与 FACTORY 的关系
原文:
FACTORY 负责处理对象生命周期的开始,而 REPOSITORY 帮助管理生命周期的中间和结束。
从领域驱动设计的角度来看,FACTORY 和 REPOSITORY 具有完全不同的职责。FACTORY 负责制造新对象,而 REPOSITORY 负责查找已有对象。REPOSITORY 应该让客户感觉到那些对象就好像驻留在内存中一样。对象可能必须被重建(的确,可能会创建一个新实例),但它是同一个概念对象,仍旧处于生命周期的中间。
# 为关系数据库设计对象
# 第 7 章 使用语言:一个扩展的示例
# 货物运输系统简介
这章用了一个货物运输系统的设计来作为之前章节所有内容的也给串联。
# 隔离领域:引入应用层
原文:为了防止领域的职责与系统的其他部分混杂在一起,我们应用 LAYERED ARCHITECTURE 把领域层划分出来。
# 将 ENTITY 和 VALUE OBJECT 区别开
# 设计运输领域中的关联
# AGGREGATE 边界
# 选择 REPOSITORY
# 场景走查
# 应用程序特性举例:更改 Cargo 的目的地
# 应用程序特性举例:重复业务
# 对象的创建
# Cargo 的 FACTORY 和构造函数
# 添加 Handling Event
# 停一下,重构:Cargo AGGREGATE 的另一种设计
# 运输模型中的 MODULE
# 引入新特性:配额检查
# 连接两个系统
# 进一步完善模型:划分业务
# 性能优化
# 小结
# 第三部分 通过重构来加深理解
原文:
要想成功地开发出实用的模型,需要注意以下 3 点。
复杂巧妙的领域模型是可以实现的,也是值得我们去花费力气实现的。 这样的模型离开不断的重构是很难开发出来的,重构需要领域专家和热爱学习领域知识的开发人员密切参与进来。 要实现并有效地运用模型,需要精通设计技巧。
个人理解:
# 重构的层次
原文:
重构就是在不改变软件功能的前提下重新设计它。开发人员无需在着手开发之前做出详细的设计决策,只需要在开发过程中不断小幅调整设计即可,这不但能够保证软件原有的功能不变,还可使整个设计更加灵活易懂。自动化的单元测试套件能够保证对代码进行相对安全的试验。这个过程解放了开发人员,使他们不再需要提前考虑将来的事情。
然而,几乎所有关于重构的文献都专注于如何机械地修改代码,以使其更具可读性或在非常细节的层次上有所改进。如果开发人员能够看准时机,利用成熟的设计模式进行开发,那么“通过重构得到模式”(refactoring to patterns)这种方式就可以让重构过程更上一层楼。
# 深层模型
原文:
深层模型能够穿过领域表象,清楚地表达出领域专家们的主要关注点以及最相关的知识。以上定义并没有涉及抽象。事实上,深层模型通常含有抽象元素,但在切中问题核心的关键位置也同样会出现具体元素。
# 深层模型/柔性设计
原文:
柔性设计除了便于修改,还有助于改进模型本身。MODEL-DRIVEN DESIGN 需要以下两个方面的支持:深层模型使设计更具表现力;同时,当设计的灵活性可以让开发人员进行试验,而设计又能清晰地表达出领域含义时,那么这个设计实际上就能够将开发人员的深层理解反馈到整个模型发现的过程中。这段反馈回路是很重要的,因为我们所寻求的模型并不仅仅只是一套好想法:它还应该是构建系统的基础
# 发现过程
原文:
你需要富有创造力,不断地尝试,不断地发现问题才能找到合适的方法为你所发现的领域概念建模,但有时你也可以借用别人已建好的模式。这些模式并不是现成的解决方案,但是它们可以帮助我们消化领域知识并缩小研究范围。
有时,当我们拥有了 MODEL-DRIVEN DESIGN 和显式概念,就能够产生突破。我们有机会使软件更富表达力、更加多样化,甚至会使它变得超乎我们的想象。这可以为软件带来新特性,或者意味着我们可以用简单灵活的方式来表达更深层次的模型,从而替换掉大段死板的代码。尽管这种突破不会时常出现,但它们非常有价值,当我们有机会进行突破时,一定要懂得识别并抓住机会。
# 第 8 章 突破
原文:
重构的投入与回报并非呈线性关系。
# 一个关于突破的故事
作者讲了一个故事来说明突破的情形以及告知其中的理论
# 华而不实的模型
# 突破
# 更深层模型
# 冷静决策
# 成果
# 机遇
原文:
当突破带来更深层的模型时,通常会令人感到不安。与大部分重构相比,这种变化的回报更多,风险也更高。而且突破出现的时机可能很不合时宜。
# 关注根本
原文:
不要试图去制造突破,那只会使项目陷入困境。通常,只有在实现了许多适度的重构后才有可能出现突破。在大部分时间里,我们都在进行微小的改进,而在这种连续的改进中模型深层含义也会逐渐显现
# 越来越多的新理解
# 第 9 章 将隐式概念转变为显式概念
原文:
深层建模听起来很不错,但是我们要如何实现它呢?深层模型之所以强大是因为它包含了领域的核心概念和抽象,能够以简单灵活的方式表达出基本的用户活动、问题以及解决方案。深层建模的第一步就是要设法在模型中表达出领域的基本概念。随后,在不断消化知识和重构的过程中,实现模型的精化。但是实际上这个过程是从我们识别出某个重要概念并且在模型和设计中把它显式地表达出来的那个时刻开始的。
若开发人员识别出设计中隐含的某个概念或是在讨论中受到启发而发现一个概念时,就会对领域模型和相应的代码进行许多转换,在模型中加入一个或多个对象或关系,从而将此概念显式地表达出来。
# 概念挖掘
# 倾听语言
原文:
倾听领域专家使用的语言。
# 检查不足之处
原文:
你所需要的概念并不总是浮在表面上,也绝不仅仅是通过对话和文档就能让它显现出来。有些概念可能需要你自己去挖掘和创造。要挖掘的地方就是设计中最不足的地方,也就是操作复杂且难于解释的地方。每当有新的需求时,似乎都会让这个地方变得更加复杂。
# 思考矛盾之处
原文:
由于经验和需求的不同,不同的领域专家对同样的事情会有不同的看法。即使是同一个人提供的信息,仔细分析后也会发现逻辑上不一致的地方。在挖掘程序需求的时候,我们会不断遇到这种令人烦恼的矛盾,但它们也为深层模型的实现提供了重要线索。有些矛盾只是术语说法上的不一致,有些则是由于误解而产生的。
# 查阅书籍
原文:
# 尝试,再尝试
# 如何为那些不太明显的概念建模
原文:
面向对象范式会引导我们去寻找和创造特定类型的概念。所有事物(即使是像“应计费用”这种非常抽象的概念)及其操作行为是大部分对象模型的主要部分。
# 显式的约束
原文:
约束是模型概念中非常重要的类别。它们通常是隐含的,将它们显式地表现出来可以极大地提高设计质量。
如果约束的存在掩盖了对象的基本职责,或者如果约束在领域中非常突出但在模型中却不明显,那么就可以将其提取到一个显式的对象中,甚至可以把它建模为一个对象和关系的集合
# 将过程建模为领域对象
原文:
我们都不希望过程变成模型的主要部分。对象是用来封装过程的,这样我们只需考虑对象的业务目的或意图就可以了。
如果过程的执行有多种方式,那么我们也可以用另一种方法来处理它,那就是将算法本身或其中的关键部分放到一个单独的对象中。这样,选择不同的过程就变成了选择不同的对象,每个对象都表示一种不同的 STRATEGY。
约束和过程是两大类模型概念,当我们用面向对象语言编程时,不会立即想到它们,然而它们一旦被我们视为模型元素,就真的可以让我们的设计更为清晰。
“规格”提供了用于表达特定类型的规则的精确方式,它把这些规则从条件逻辑中提取出来,并在模型中把它们显式地表示出来。
# SPECIFICATION
原文:
那些使用逻辑编程范式的开发人员会用一种不同的方式来处理这种情况。这种规则被称为谓词。谓词是指计算结果为“真”或“假”的函数,并且可以使用操作符(如 AND 和 OR)把它们连接起来以表达更复杂的规则。
业务规则通常不适合作为 ENTITY 或 VALUE OBJECT 的职责,而且规则的变化和组合也会掩盖领域对象的基本含义。但是将规则移出领域层的结果会更糟糕,因为这样一来,领域代码就不再表达模型了。
**SPECIFICATION(规格)**中声明的是限制另一个对象状态的约束,被约束对象可以存在,也可以不存在。SPECIFICATION 有多种用途,其中一种体现了最基本的概念,这种用途是:SPECIFICATION 可以测试任何对象以检验它们是否满足指定的标准。
个人理解:
作者的意思将一些公共的逻辑判断也统一抽象,因为此类逻辑可以多个合并组成更强到的逻辑,并且这些东西也可以用更好的方式来实现,比如规则引擎,但是作业也说了要是完全用对象来实现这些逻辑也是一个大工程。
# SPECIFICATION 的应用和实现
原文:
SPECIFICATION 最有价值的地方在于它可以将看起来完全不同的应用功能统一起来。出于以下 3 个目的中的一个或多个,我们可能需要指定对象的状态。
验证对象,检查它是否能满足某些需求或者是否已经为实现某个目标做好了准备。 从集合中选择一个对象(如上述例子中的查询过期发票)。 指定在创建新对象时必须满足某种需求。
关系数据库具有强大的查询能力。我们如何才能充分利用这种能力来有效解决这一问题,同时又能保留 SPECIFICATION 模型呢?MODEL-DRIVEN DESIGN 要求模型与实现保持同步,但它同时也让我们可以自由选择能够准确捕捉模型意义的实现方式。幸运的是,SQL 是用于编写 SPECIFICATION 的一种很自然的方式。
一些对象关系映射框架提供了用模型对象和属性来表达这种查询的方式,并在基础设施层中创建实际的 SQL 语句。
如果无法把 SQL 语句创建到基础设施中,还可以重写一个专用的查询方法并把它添加到 Invoice Repository 中,这样就把 SQL 语句从领域对象中分离出来了。为了避免在 REPOSITORY 中嵌入规则,必须采用更为通用的方式来表达查询,这种方式不捕捉规则但是可以通过组合或放置在上下文中来表达规则(在这个例子中,使用的是双分派模式)。
如果不使用 SPECIFICATION,可以编写一个生成器,其中包含可创建所需对象的过程或指令集。这种代码隐式地定义了生成器的行为。
反过来,我们也可以使用描述性的 SPECIFICATION 来定义生成器的接口,这个接口就显式地约束了生成器产生的结果。这种方法具有以下几个优点。
生成器的实现与接口分离。SPECIFICATION 声明了输出的需求,但没有定义如何得到输出结果。 接口把规则显式地表示出来,因此开发人员无需理解所有操作细节即可知晓生成器会产生什么结果。而如果生成器是采用过程化的方式定义的,那么要想预测它的行为,唯一的途径就是在不同的情况下运行或去研究每行代码。 接口更为灵活,或者说我们可以增强其灵活性,因为需求由客户给出,生成器唯一的职责就是实现 SPECIFICATION 中的要求。 最后一点也很重要。这种接口更加便于测试,因为接口显式地定义了生成器的输入,而这同时也可用
如何通过更巧妙的模型使“最简单却可能非常最有效的事物”成为可能。
个人理解:作者的三个目的其实就是我们代码编程中对应的三个方面:验证,选择,以及后续逻辑的实现。其中我们常用的关系型数据库自然和规格运用是一种很自然的方式,因为我们无非就是从数据通过条件拿数据,而规格模型字段与数据库表结构统一映射,通过数据库映射层来组装查询SQL。我们对双分派模式进行一个简单表述:
双分派模式(Double Dispatch)
双分派模式是一种面向对象编程中的行为分派技术,常用于在运行时动态决定对象之间交互的实际方法调用。它是 多态性 的一个重要扩展,解决了简单单分派无法满足的某些场景需求。
核心思想
双分派的关键是将方法的选择依赖于 两个对象的运行时类型,而非单一对象。通过将一个方法调用分派到接收者对象,同时再分派到参数对象,使得系统能够更精确地匹配需要执行的方法。
实现机制
- 双分派:在第一次调用接收者的方法时,将方法选择的控制权交给参数对象,基于参数对象的类型执行对应的逻辑。
本作者则让我们在不使用他提出的SPECIFICATION的概念下,运用生成器模式来完成SPECIFICATION的要求。
# 第 10 章 柔性设计
原文:
软件的最终目的是为用户服务。但首先它必须为开发人员服务。在强调重构的软件开发过程中尤其如此。随着程序的演变,开发人员将重新安排并重写每个部分。他们会把原有的领域对象集成到应用程序中,也会让它们与新的领域对象进行集成。甚至几年以后,负责维护的程序员还将修改和扩充代码。
当具有复杂行为的软件缺乏良好的设计时,重构或元素的组合会变得很困难。一旦开发人员不能十分肯定地预知计算的全部含意,就会出现重复。当设计元素都是整块的而无法重新组合的时候,重复就是一种必然的结果。我们可以对类和方法进行分解,这样可以更好地重用它们,但这些小部分的行为又变得很难跟踪。如果软件没有一个条理分明的设计,那么开发人员不仅不愿意仔细地分析代码,他们更不愿意修改代码,因为修改代码会产生问题——要么加重了代码的混乱状态,要么由于某种未预料到的依赖而破坏了某些东西。在任何一种系
统中(除非是一些非常小的系统),这种不稳定性使我们很难开发出丰富的功能,而且限制了重构和迭代式的精化。
为了使项目能够随着开发工作的进行加速前进,而不会由于它自己的老化停滞不前,设计必须要让人们乐于使用,而且易于做出修改。这就是柔性设计(supple design)。柔性设计是对深层建模的补充。一旦我们挖掘出隐式概念,并把它们显示地表达出来之后,就有了原料。通过迭代循环,我们可以把这些原料打造成有用的形式:建立的模型能够简单而清晰地捕获主要关注点;其设计可以让客户开发人员真正使用这个模型。在设计和代码的开发过程中,我们将获得新的理解,并通过这些理解改善模型概念。我们一次又一次回到迭代循环中,通过重构得到更深刻的理解。
很多过度设计(overengineering)借着灵活性的名义而得到合理的外衣。但是,过多的抽象层和间接设计常常成为项目的绊脚石。看一下真正为用户带来强大功能的软件设计,你常常会发现一些简单的东西。简单并不容易做到。为了把创建的元素装配到复杂系统中,而且在装配之后仍然能够理解它们,必须坚持模型驱动的设计方法,与此同时还要坚持适当严格的设计风格。要创建或使用这样的设计,可能需要我们掌握相对熟练的设计技巧。
开发人员扮演着两个角色,而设计必须要为这两个角色服务。
一个角色是客户开发人员,负责将领域对象组织成应用程序代码或其他领域层代码,以便发挥设计的功能。柔性设计能够揭示深层次的底层模型,并把它潜在的部分明确地展现出来。客户开发人员可以灵活地使用一个最小化的、松散耦合的概念集合,并用这些概念来表示领域中的众多场景。设计元素非常自然地组合到一起,其结果也是健壮的,可以被清晰地刻画出来,而且也是可以预知的。
同样重要的是,设计也必须为那些修改代码的开发人员服务。为了便于修改,设计必须易于理解,必须把客户开发人员正在使用的同一个底层模型表示出来。我们必须按照领域深层模型的轮廓进行设计,以便大部分修改都可以灵活地完成。代码的结果必须是完全清晰明了的,这样才容易预见到修改的影响。
个人理解:
我们所有的程序在一开始通常都不是柔性设计,并且大多数功能需求由于时间和其他条件的限制也不能让开发人员去考虑所谓的柔性设计, 并且确实有些人为了某些功能过度设计导致程序难维护难解读,我们开发人员为了平衡设计,首先做好以领域设计思想为切入点,在深度建模的基础上开发高内聚低耦合的程序,并且以细粒度模块来组装成复杂多变的众多场景,就好像玩乐高,我们用小的积木搭建各种场景,其次我们设计就要考虑运维的难度,简而言之:写自己对于领取驱动能力最好的代码,让别人解读最简单的代码。
# INTENTION-REVEALING INTERFACES 模式
原文:
在领域驱动的设计中,我们希望看到有意义的领域逻辑。如果代码只是在执行规则后得到结果,而没有把规则显式地表达出来,那么我们就不得一步一步地去思考软件的执行步骤。那些只是运行代码然后给出结果的计算——没有显式地把计算逻辑表达出来,也有同样的问题。如果不把代码与模型清晰地联系起来,我们很难理解代码的执行效果,也很难预测修改代码的影响。
个人理解:
意图显露的接口设计应该清晰地表达出开发者的意图,让接口名称及其使用方式能准确传递背后的业务含义和逻辑,而无需阅读实现细节。通过这种设计,可以让代码更加可读、可维护,同时帮助团队更容易理解领域模型。并且能够改善其他人员对于抽象的理解。
# SIDE-EFFECT-FREE FUNCTION
原文:
我们可以宽泛地把操作分为两个大的类别:命令和查询。查询是从系统获取信息,查询的方式可能只是简单地访问变量中的数据,也可能是用这些数据执行计算。命令(也称为修改器)是修改系统的操作(举一个简单的例子,设置变量)。在标准英语中,“副作用”这个词暗示着“意外的结果”,但在计算机科学中,任何对系统状态产生的影响都叫副作用。这里为了便于讨论,我们把它的含义缩小一下,任何对未来操作产生影响的系统状态改变都可以称为副作用。
在一个复杂的设计中,元素之间的交互同样也会产生无法预料的结果。副作用这个词强调了这种交互的不可避免性。
返回结果而不产生副作用的操作称为函数。一个函数可以被多次调用,每次调用都返回相同的值。一个函数可以调用其他函数,而不必担心这种嵌套的深度。函数比那些有副作用的操作更易于测试。由于这些原因,使用函数可以降低风险。
显然,在大多数软件系统中,命令的使用都是不可避免的,但有两种方法可以减少命令产生的问题。首先,可以把命令和查询严格地放在不同的操作中。确保导致状态改变的方法不返回领域数据,并尽可能保持简单。在不引起任何可观测到的副作用的方法中执行所有查询和计算。
总是有一些替代的模型和设计,它们不要求对现有对象做任何修改。相反,它们创建并返回一个 VALUE OBJECT,用于表示计算结果。VALUE OBJECT 可以在一次查询的响应中被创建和传递,然后被丢弃——不像 ENTITY,实体的生命周期是受到严格管理的。
VALUE OBJECT 是不可变的,这意味着除了在创建期间调用的初始化程序之外,它们的所有操作都是函数。像函数一样,VALUE OBJECT 使用起来很安全,测试也很简单。如果一个操作把逻辑或计算与状态改变混合在一起,那么我们就应该把这个操作重构为两个独立的操作[Fowler1999, p. 279]。但从定义上来看,这种把副作用隔离到简单的命令方法中的做法仅适用于 ENTITY。在完成了修改和查询的分离之后,可以考虑再进行一次重构,把复杂计算的职责转移到 VALUE OBJECT 中。通过派生出一个 VALUE OBJECT(而不是改变现有状态),或者通过把职责完全转移到一个 VALUE OBJECT 中,往往可以完全消除副作用。
尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令(引起明显的状态改变的方法)隔离到不返回领域信息的、非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到 VALUE OBJECT 中,这样可以进一步控制副作用。
个人理解:
SIDE-EFFECT-FREE FUNCTION(无副作用函数) 是一种编程概念,指的是在执行函数时,不会对外部状态产生任何影响,且函数的输出仅由输入参数决定。这样设计的函数具有高度的可预测性、可重用性和易测试性,是函数式编程和领域驱动设计 (DDD) 中的重要原则之一。
# 核心特点
无副作用
- 不修改外部状态(如全局变量、数据库、文件等)。
- 不产生不可见的行为(如隐式地改变系统状态或触发事件)。
- 不依赖外部可变状态。
纯输入-输出关系
- 函数的输出完全由输入决定。
- 相同的输入总是返回相同的输出。
独立性
- 函数执行不依赖外部环境或上下文。
- 可以在任何时候、任何地方调用而不影响其他逻辑。
# 优点
可测试性
- 无需模拟外部状态,测试只需验证输入和输出是否匹配。
- 易于编写单元测试。
可重用性
- 因为不依赖外部环境,函数可以在不同上下文中安全复用。
易理解
- 函数行为是透明的,逻辑清晰且易于推断。
并发安全
- 由于不修改共享状态,无副作用函数天然适合多线程或并发环境。
可缓存
- 因为相同输入总是产生相同输出,结果可以安全地缓存。
# 应用场景
- 函数式编程
- 无副作用是函数式编程的核心原则之一。
- 数据处理
- 纯数据操作(如映射、过滤)常使用无副作用函数。
- 领域驱动设计
- 用于领域模型中的计算或校验逻辑。
- 微服务或分布式系统
- 无副作用函数简化了状态管理和调试。
# 示例
# 有副作用的函数
counter = 0
def increment():
global counter
counter += 1
return counter
2
3
4
5
6
问题:
- 函数依赖外部的
counter
变量。 - 每次调用都会改变外部状态,且输出不确定。
# 无副作用的函数
def add(x, y):
return x + y
2
特点:
- 输入是
x
和y
,输出仅由它们决定。 - 没有修改外部变量或依赖外部状态。
# 领域驱动设计中的作用
在 DDD(领域驱动设计) 中,无副作用函数通常用于:
- VALUE OBJECT 的计算和比较。
- 领域逻辑 中的规则校验和转换。
- 任何与状态修改无关的逻辑。
# 示例
领域逻辑:根据价格计算折扣。
有副作用(不推荐):
discount = 0
def apply_discount(price):
global discount
discount = price * 0.1
return price - discount
2
3
4
5
6
无副作用(推荐):
def calculate_discounted_price(price, discount_rate):
return price - price * discount_rate
2
说人话就是:你写的函数没有隐藏逻辑以及对于除了传入参数和输出响应参数之外的任何第三方依赖。我们在开发过程中最好帮逻辑封装到一个一个的函数中,来降低副作用,并且最好通过VALUE OBJECT来传递。
# ASSERTIONS 模式
原文:
INTENTION-REVEALING INTERFACE 清楚地表明了用途,SIDE-EFFECT-FREE FUNCTION 和 ASSERTION 使我们能够更准确地预测结果,因此封装和抽象更加安全。
个人理解:
ASSERTIONS(断言)模式的核心思想是显式地验证系统状态或输入条件是否符合预期,帮助开发者快速发现潜在的错误或不一致性。这种方法强调代码自我验证,确保代码逻辑的正确性和可维护性。
# CONCEPTUAL CONTOUR
原文:
把设计元素(操作、接口、类和 AGGREGATE)分解为内聚的单元,在这个过程中,你对领域中一切重要划分的直观认识也要考虑在内。在连续的重构过程中观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层 CONCEPTUAL CONTOUR。使模型与领域中那些一致的方面(正是这些方面使得领域成为一个有用的知识体系)相匹配。
我们的目标是得到一组可以在逻辑上组合起来的简单接口,使我们可以用 UBIQUITOUS LANGUAGE 进行合理的表述,并且使那些无关的选项不会分散我们的注意力,也不增加维护负担。但这通常是通过重构才能得到的结果,很难在前期就实现。而且如果仅仅是从技术角度进行重构,可能永远也不会出现这种结果;只有通过重构得到更深层的理解,才能实现这样的目标。
INTENTION-REVEALING INTERFACE 使客户能够把对象表示为有意义的单元,而不仅仅是一些机制。SIDE-EFFECT-FREE FUNCTION 和 ASSERTION 使我们可以安全地使用这些单元,并对它们进行复杂的组合。CONCEPTUAL CONTOUR 的出现使模型的各个部分变得更稳定,也使得这些单元更直观,更易于使用和组合。
个人理解:
CONCEPTUAL CONTOUR(概念轮廓) 是领域驱动设计(DDD)中关于模型表达和边界清晰度的重要原则。它强调模型应该自然地映射到领域中的概念边界,使得模型更具表达力、清晰性和可维护性。概念轮廓这概念的名称我们也能看出来他是要一个与其他概念的分界,其中我们前面一直再说的高内聚,低耦合,然后概念轮廓则是将之前涉及的到概念意图显露的接口,以及无副作用函数,以及断言有进行了一次组装建模。集腋成裘,完成一个大的功能,然后底层的各个组合元素又是稳固清晰的。
# STANDALONE CLASS
原文:
低耦合是对象设计的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变得完全独立了,这就使得我们可以单独地研究和理解它。每个这样的独立类都极大地减轻了因理解 MODULE 而带来的负担。
尽力把最复杂的计算提取到 **STANDALONE CLASS(独立的类)**中,实现此目的的一种方法是从存在大量依赖的类中将 VALUE OBJECT 建模出来。
低耦合是减少概念过载的最基本办法。独立的类是低耦合的极致。
个人理解:
STANDALONE CLASS(独立类) 是一种设计概念,强调类应该具备单一职责,且不依赖于其他类的上下文,从而提高代码的可复用性、可测试性和维护性。就好比我们所有的对于数据的访问基础设施层的所有service,以及mapper 都应该是一个独立类,如果有复杂的查询则是提到另一个上下文中组合实现,用VALUE OBJECT 建模承接。
# CLOSURE OF OPERATION
原文:
当我们对集合中的任意两个元素组合时,结果仍在这个集合中,这就叫做闭合操作。
依赖是必然存在的,当依赖是概念的一个基本属性时,它就不是坏事。
个人理解:
运算封闭性有两个概念:
数学概念中运算封闭性指一个操作的结果仍然属于操作的输入范围。
在编程中,闭包(Closure)操作确保函数或方法的返回值与输入具有相同的类型或属于相同的集合。
CLOSURE OF OPERATION 保证操作的结果仍然属于与输入相同的类型或集合,提供设计的一致性和可组合性。在编程中,运算封闭性经常出现在 VALUE OBJECT 的操作中,确保领域建模中的一致性和完整性,同时简化了代码逻辑。并且闭包操作能够促进链式调用。
# 声明式设计
个人理解:
声明式设计(Declarative Design)是一种设计和编程范式,强调描述“做什么”而不是如何做,使代码更具可读性和表达力。这种设计方法常见于函数式编程、配置文件、查询语言(如 SQL)等领域。
# 核心特点
关注目标
- 在声明式设计中,开发者描述想要达成的目标或结果,而非具体的实现步骤。
- 设计更接近于业务逻辑和需求本身。
隐藏实现细节
- 具体的操作流程或算法由底层系统、框架、引擎等负责执行。
- 提高了代码的抽象性和可维护性。
代码更简洁
- 避免繁琐的命令式步骤,代码往往更简洁直观,逻辑更易于理解。
# 声明式与命令式的区别
声明式设计 | 命令式设计 |
---|---|
关注做什么 | 关注如何做 |
强调目标和结果 | 强调步骤和流程 |
高度抽象,简洁清晰 | 细节繁琐,需要逐步执行 |
例如:SQL 查询、React 声明式语法 | 例如:for 循环、过程式编程 |
示例:
SQL 声明式查询
SELECT name FROM users WHERE age > 18;
1- 描述了“选择所有年龄大于18的用户的名字”,不关心如何遍历数据、筛选或排序。
命令式实现(Java)
List<String> result = new ArrayList<>(); for (User user : users) { if (user.getAge() > 18) { result.add(user.getName()); } }
1
2
3
4
5
6- 逐步执行遍历、判断和添加操作,显式定义“如何做”。
# 声明式设计的优势
提高可读性
- 代码更接近自然语言和业务逻辑,易于理解和沟通。
降低耦合性
- 屏蔽实现细节,逻辑与执行过程解耦。
增强可维护性
- 修改目标时无需关心具体实现细节,只需调整需求或规则。
提高开发效率
- 依赖框架或工具执行具体操作,减少手动编码。
# 应用场景
数据库查询:SQL 是典型的声明式语言,开发者只需描述需要什么数据。
UI 框架:例如 React、Vue.js 中的声明式组件语法。
<button onClick={handleClick}>Click me</button>
1- 描述按钮的功能和行为,而不关心底层如何实现事件绑定。
函数式编程:利用高阶函数(如
map
、filter
、reduce
)处理数据。List<String> names = users.stream() .filter(user -> user.getAge() > 18) .map(User::getName) .collect(Collectors.toList());
1
2
3
4配置文件:使用 YAML、JSON、XML 等描述系统行为或规则。
自动化脚本:如 CI/CD 流水线配置、基础设施即代码(IaC)工具。
# 总结
声明式设计是一种通过描述目标和结果而非具体步骤的设计方法,强调高抽象、低耦合和可维护性。它更关注业务逻辑和意图,让系统或框架负责实现具体操作。典型应用场景包括 SQL 查询、函数式编程、UI 框架和配置文件。
# 声明式设计风格
# 切入问题的角度
# 分割子领域
原文:
重点突击某个部分,使设计的一个部分真正变得灵活起来,这比分散精力泛泛地处理整个系统要有用得多。
个人理解:
先了解各个模块,然后在整体了解。
# 尽可能利用已有的形式
原文:
我们不能把从头创建一个严密的概念框架当作一项日常的工作来做。在项目的生命周期中,我们有时会发现并精炼出这样一个框架。但更常见的情况是,可以对你的领域或其他领域中那些建立已久的概念系统加以修改和利用,其中有些系统已经被精化和提炼达几个世纪之久。
个人理解:
能在现有的框架概念下做精炼建模是最好的,因为时间累积下来的经验,总比开发人员的个人理解更透彻,不要一来看啥都不爽,看啥都想重构,重构不是把老代码干掉写一套新的,而是对于领域知识的再精炼,模型再优化,拓展性更高,性能更好。我觉得重构这种活儿需要给一个老员工,负责该项目比较久的人员,而不是随便拉一个人开始重构。
# 第 11 章 应用分析模式
原文:
分析模式是一种概念集合,用来表示业务建模中的常见结构。它可能只与一个领域有关,也可能跨越多个领域。
在一个成熟的项目上,模型选择往往是根据实用经验做出的。人们已经尝试了各种组件的多种实现方法。其中的一些实现已经被采用,有些甚至已经到了维护阶段。这些经验可以帮助人们避免很多问题。分析模式的最大作用是借鉴其他项目的经验,把那些项目中有关设计方向和实现结果的广泛讨论与当前模型的理解结合起来。脱离具体的上下文来讨论模型思想不但难以落地,而且还会造成分析与设计严重脱节的风险,而这一点正是 MODEL-DRIVEN DESIGN 坚决反对的。
我们应该把所有分析模式的知识融入到知识消化和重构的过程中,从而形成更深刻的理解,并促进开发。当我们应用一种分析模式时,所得到的结果通常与该模式的文献中记载的形式非常相像,只是因具体情况不同而略有差异。但有时完全看不出这个结果与分析模式本身有关,然而这个结果仍然是受该模式思想的启发而得到的。
很多对象模型都有文献资料可查,其中有些对象模型专门用于某个行业中的某种应用,而有些则是通用模型。大部分对象模型都有助于开阔思路,但只有为数不多的一些模型精辟地阐述了选择这些模式的原理和使用的结果,而这些才是分析模式的精华所在。这些精化后的分析模式大部分都很有价值,有了它们,可以免去一次次的重复开发工作。尽管我们不大可能归纳出 一个包罗万象的分析模式类目,但针对具体行业的类目还是能够开发出来的。而且在一些跨越多个应用的领域中适用的模式可以被广泛共享。
这种对已组织好的知识的重复利用完全不同于通过框架或组件进行的代码重用,但是二者唯一的共同点是它们都提供了一种新思路的萌芽,而这种新思路先前可能并不十分明晰。一个模型,甚至一个通用框架,都是一个完整的整体,而分析则相当于一个工具包,它被应用于模型的一些部分。分析模式专注于一些最关键和最艰难的决策,并阐明了各种替代和选择方案。它们提前预测了一些后期结果,而如果单靠我们自己去发现这些结果,可能会付出高昂的代价。
个人理解:
应用分析模式通过将通用的设计经验转化为可重用的模式,帮助开发者快速解决软件开发中的常见问题。它在领域建模、系统设计和架构实现中起到了桥梁作用,提高了系统的开发效率和设计质量。站在巨人的肩膀上作事,积极采纳业内领先知识不可耻,其实我们所谓的抄袭再另一方面说也可能是最快解决类似问题的高效方案。借鉴的过程中也是也不是完全的的照搬照抄,其实其中的借鉴过程也是一门技术活儿。
# 第 12 章 将设计模式应用于模型
原文:
设计模式与领域模式之间有什么区别?
个人理解:
维度 | 设计模式 | 领域模式 |
---|---|---|
关注点 | 技术实现、代码结构 | 业务建模、领域表达 |
应用层次 | 技术层 | 领域层 |
目标 | 优化代码复用性、可扩展性 | 表达领域概念,反映业务语义 |
示例 | 单例模式、策略模式 | 聚合根、领域事件 |
开发阶段 | 代码设计与实现 | 需求分析与领域建模 |
设计模式和领域模式并不矛盾,而是可以互补结合:
在领域模型构建的过程中,设计模式可以帮助实现高质量的代码结构,从而更好地表达领域模型的语义。
# STRATEGY(也称为 POLICY)
原文:
通常,作为设计模式的 STRATEGY 侧重于替换不同算法的能力,而当其作为领域模式时,其侧重点则是表示概念的能力,这里的概念通常是指过程或策略规则。
我们在领域层中使用技术设计模式时,必须认识到这样做的另外一种动机,也是它的另一层含义。当所使用的 STRATEGY 对应于某种实际的业务策略时,模式就不再仅仅是一种有用的实现技术了(但它在实现方面的价值并未改变)。
# COMPOSITE
# 为什么没有介绍 FLYWEIGHT
# 第 13 章 通过重构得到更深层的理解
原文:
通过重构得到更深层的理解是一个涉及很多方面的过程。我们有必要暂停一下,把一些要点归纳到一起。有三件事情是必须要关注的:
以领域为本; 用一种不同的方式来看待事物; 始终坚持与领域专家对话。
一提到传统意义上的重构,我们头脑中就会出现这样一幅场景:一两位开发人员坐在键盘前面,发现一些代码可以改进,然后立即动手修改代码(当然还要用单元测试来验证结果)。这个过程应该一直进行下去,但它并不是重构过程的全部。
个人理解:
重构不是改进部分代码,也不是看不爽别人的实现方式,推翻自己再写一套,改进代码只是重构的一部分。
# 开始重构
原文:
获得深层理解的重构可能出现在很多方面。一开始有可能是为了解决代码中的问题——一段复杂或笨拙的代码。但开发人员并没有使用(代码重构所提供的)标准的代码转换,相反,他们认为问题的根源在于领域模型。或许是领域中缺少一个概念,或许是某个关系发生了错误。
与传统重构观点不同的是,即使在代码看上去很整洁的时候也可能需要重构,原因是模型的语言没有与领域专家保持一致,或者新需求不能被自然地添加到模型中。重构的原因也可能来自学习:当开发人员通过学习获得了更深刻的理解,从而发现了一个得到更清晰或更有用的模型的机会。
如何找到问题的病灶往往是最难和最不确定的部分。在这之后,开发人员就可以系统地找出新模型的元素。他们可以与同事和领域专家一起进行头脑风暴,也可以充分利用那些已经对知识做了系统性总结的分析模式或设计模式。
个人理解:
重构不是改进部分代码,也不是看不爽别人的实现方式,推翻自己再写一套,改进代码只是重构的一部分。即使代码上的优雅也可能需要重构,结合领域专家,也可能是业务人员的专业知识,不脱离业务知识,再结合自身对于业务的理解,多沟通,发现现在模型的根本性问题,然后结合分析模型,以及设计模式来重新建模。
# 探索团队
原文:
不管问题的根源是什么,下一步都是要找到一种能够使模型表达变得更清楚和更自然的改进方案。这可能只需要做一些简单、明显的修改,只需几小时即可完成。在这种情况下,所做的修改类似于传统重构。但寻找新模型可能需要更多时间,而且需要更多人参与。
修改的发起者会挑选几位开发人员一起工作,这些开发人员应该擅长思考该类问题,了解领域,或者掌握深厚的建模技巧。如果涉及一些难以捉摸的问题,他们还要请一位领域专家加入。这个由 4 ~ 5 人组成的小组会到会议室或咖啡厅进行头脑风暴,时间为半小时至一个半小时。在这个过程中,他们画一些 UML 草图,并试着用对象来走查场景。他们必须保证主题专家(subjectmatter expert)能够理解模型并认为模型有用。当发现了一些令他们满意的新思路后,他们就回去编码,或者决定再多考虑几天,先回去做点别的事情。几天之后,这个小组再次碰头,重复上面的过程。这时,他们已经对前几天的想法有了更深入的理解,因此更加自信了,并且得出了一些结论。他们回到计算机前,开始对新设计进行编码。
要想保证这个过程的效率,需要注意几个关键事项。
自主决定。可以随时组成一个小的团队来研究某个设计问题。这个团队只工作几天,然后就可以解散了。这种团队没有长期存在的必要,也不必有复杂的组织结构。 注意范围和休息。在几天内召开两三次短会就应该能够产生一个值得尝试的设计。工作拖得太长并没什么好处。如果讨论毫无进展,可能是一次讨论的内容太多了。选一个较小的设计方面,集中讨论它。
练习使用 UBIQUITOUS LANGUAGE。让其他团队成员(特别是主题专家)参与头脑风暴会议是练习和精化 UBIQUITOUS LANGUAGE 的好机会。这样,原来的开发人员可以得到更完善的 UBIQUITOUS LANGUAGE,并反映到编码中。
个人理解:
重构所需要人力和时间,而人员的选择很重要,人不一定要多,而应该要熟悉或者擅长此领域模型的设计,或者说掌握了深厚的建模技巧,对于大家都不熟悉的领域,最好是寻求外在的领域专家和业务人员进行头脑风暴,整个过程按照作者的关键事项:自主决定,注意范围和休息,使用统一语言。
# 借鉴先前的经验
原文:
我们没有必要总去做一些无谓的重复工作。用于查找缺失概念或改进模型的头脑风暴过程具有巨大的作用,通过这个过程可以收集来自各个方面的想法,并把这些想法与已有知识结合起来。随着知识消化的不断开展,就能找到当前问题的答案。
我们可以从书籍和领域自身的其他知识源获得思路。尽管相关领域的人员可能还没有创建出适合运行软件的模型,但他们可能已经把概念很好地组织到了一起,并发现了一些有用的抽象。把这些知识结合到知识消化过程中,可以更快速地得到更丰富的结果,而且这个结果也更为领域专家们所熟悉。
有时我们可以从分析模式中汲取他人的经验。这些经验对于帮助我们读懂领域起到了一定的作用,但分析模式是专门针对软件开发的,因此应该直接根据我们自己在领域中实现软件的经验来利用这些模式。分析模式可以提供精细的模型概念,并帮助我们避免很多错误。但它们并不是现成的“菜谱”。它们只是为知识消化过程提供了一些供给。
随着零散知识的归纳,必须同时处理模型关注点和设计关注点。同样,这并不意味着总是需要从头开发一切。当设计模式既符合实现需求,又符合模型概念时,通常就可j以在领域层中应用这些模式。
个人理解:
很多人拿到别人写的代码总是嗤之以鼻,静下心先解读别人的代码,然后读懂逻辑,理解逻辑,然后再总结逻辑,学会站在巨人的肩膀上做事,我们对于领域模型设计的同时需要考虑实现模式的结合。不要有推倒重来的的错误看法。
# 针对开发人员的设计
原文:
软件不仅仅是为用户提供的,也是为开发人员提供的。开发人员必须把他们编写的代码与系统的其他部分集成到一起。在迭代过程中,开发人员反复修改代码。开发人员应该通过重构得到更深层的理解,这样既能够实现柔性设计,也能够从这样一个设计中获益。
个人理解:
# 重构的时机
原文:
持续重构渐渐被认为是一种“最佳实践”,但大部分项目团队仍然对它抱有很大的戒心。人们虽然看到了修改代码会有风险,还要花费开发时间,但却不容易看到维持一个拙劣设计也有风险,而且迁就这种设计也要付出代价。想要重构的开发人员往往被要求证明其重构的合理性。虽然这看似合理,但这使得一个本来就很难进行的工作变得几乎不可能完成,而且会限制重构的进行(或者人们只能暗地里进行)。软件开发并不是一个可以完全预料到后果的过程,人们无法准确地计算出某个修改会带来哪些好处,或者是不做某个修改会付出多大代价。
在探索领域的过程中、在培训开发人员的过程中,以及在开发人员与领域专家进行思想交流的过程中,必须始终坚持把“通过重构得到更深层理解”作为这些工作的一部分。因此,当发生以下情况时,就应该进行重构了
设计没有表达出团队对领域的最新理解; 重要的概念被隐藏在设计中了(而且你已经发现了把它们呈现出来的方法); 发现了一个能令某个重要的设计部分变得更灵活的机会。
个人理解:
在日常的工作中我也发现,其实很多人都追求不求有功,但求无过,所有只要重构必定有风险和时间成本,并且预期的重构成效很难评估,然而很多公司当你要重构的时候就需要关键指标进行评估重构的可行性,从开始重构开始这项工作就是一个任重而道远的事情。并且很多人对于重构的时机仅仅是一个简短的判断,看着不爽,代码写的烂开始所谓的重构,然后把逻辑再根据自己的理解又写了一遍。作者给给出了三点重构的情况:设计没有表达出团队对领域的最新理解,重要的概念被隐藏在设计中了(而且你已经发现了把它们呈现出来的方法),发现了一个能令某个重要的设计部分变得更灵活的机会。只要发现处于三种情况之一我们就可以着手想一想重构的事。但是请确保你已经完全理解现有模型沉淀的知识以及你对相关业务已经烂熟于心,或者你通过第三方能够完全了解相关的业务知识。
# 危机就是机遇
# 第四部分 战略设计
原文:
随着系统的增长,它会变得越来越复杂,当我们无法通过分析对象来理解系统的时候,就需要掌握一些操纵和理解大模型的技术了。本书的这一部分将介绍一些原则。遵循这些原则,就可以对非常复杂的领域进行建模。大部分这样的决策都需要由团队来制定,甚至需要多个团队共同协商制定。这些决策往往是把设计和策略综合到一起的结果。
战略设计原则必须指导设计决策,以便减少各个部分之间的互相依赖,在使设计意图更为清晰的同时而又不失去关键的互操作性和协同性。战略设计原则必须把模型的重点放在捕获系统的概念核心,也就是系统的“远景”上。而且在完成这些目标的同时又不能为项目带来麻烦。为了帮助实现这些目标,这一部分探索了 3 个大的主题:上下文、精炼和大型结构。
# 第 14 章 保持模型的完整性
原文:
模型最基本的要求是它应该保持内部一致,术语总具有相同的意义,并且不包含互相矛盾的规则:虽然我们很少明确地考虑这些要求。模型的内部一致性又叫做统一(unification),这种情况下,每个术语都不会有模棱两可的意义,也不会有规则冲突。除非模型在逻辑上是一致的,否则它就没有意义。
讨论首先从描绘项目当前的范围开始。BOUNDED CONTEXT(限界上下文)定义了每个模型的应用范围,而 CONTEXT MAP(上下文图)则给出了项目上下文以及它们之间关系的总体视图。这些降低模糊性的技术能够使项目更好地进行,但仅仅有它们还是不够的。一旦确立了 CONTEXT 的边界之后,仍需要持续集成这种过程,它能够使模型保持统一。
,在这个稳定的基础之上,我们就可以开始实施那些在界定和关联 CONTEXT 方面更有效的策略了——从通过共享内核(SHARED KERNEL)来紧密关联上下文,到那些各行其道(SEPARATE WAYS)地进行松散耦合的模型。
个人理解:
# 战略设计(Strategic Design)
战略设计是领域驱动设计(DDD)中的一个重要部分,主要关注的是如何在更高层次上设计系统架构,以确保系统能够有效地反映业务需求并应对复杂性。战略设计通过划分和管理系统的边界,确保各个部分能够协同工作,避免系统的复杂性过于分散或过度耦合。
# 1. 目标与核心思想
战略设计的核心思想是通过对系统的大局观的把握,帮助设计人员和开发团队明确系统架构的整体结构,解决跨领域和跨模块的复杂性问题。它通过界定不同模块和子系统的边界,划分责任,以及明确领域模型的相关关系,从而实现业务需求与技术实现的高度一致。
# 2. 主要元素
限界上下文(Bounded Context)
- 限界上下文是战略设计中的核心概念,指的是在特定的上下文中,某些概念和模型的定义和使用是明确的。在每个限界上下文内,领域模型有其清晰的边界,跨越这些边界的交互需要通过明确的接口或集成机制来完成。
- 例如,一个电商系统中,“订单”可能在不同的限界上下文中有不同的定义,在“订单管理”上下文中,订单可能包含详细的物流信息,而在“支付”上下文中,订单仅仅表示支付状态。
上下文映射(Context Mapping)
- 上下文映射是指在多个限界上下文之间,如何定义它们之间的关系。不同的上下文之间可能有不同的交互方式,这些交互方式的定义对于系统的协调性至关重要。
- 常见的上下文关系有:
- 共享内核(Shared Kernel):不同限界上下文共享某些领域模型和代码。
- 客户-供应商(Customer-Supplier):一个限界上下文的模型作为另一个上下文的客户。
- 反向控制(Conformist):一个上下文完全服从另一个上下文的模型。
核心领域(Core Domain)
- 核心领域是系统中最具竞争力的部分,也是最重要的部分。在战略设计中,需要识别出核心领域,并确保它在开发中得到优先关注和支持。核心领域的复杂性往往要求使用更加精细化的建模策略。
支撑子系统(Supporting Subsystem)
- 支撑子系统是那些不直接影响核心领域的部分,但仍然为核心领域提供支持的部分。虽然这些部分不具备业务价值的核心竞争力,但在设计时依然需要考虑其与核心领域的交互。
通用语言(Ubiquitous Language)
- 领域驱动设计强调使用通用语言,确保开发团队与业务团队之间有一致的理解。在战略设计中,通用语言用于跨越限界上下文之间的沟通,确保所有相关人员都能清晰地理解业务需求和技术实现。
# 3. 战略设计的步骤
识别并划分限界上下文(Bounded Contexts)
- 确定不同的子系统或模块,哪些部分可以作为独立的限界上下文。在每个限界上下文中,领域模型应该是清晰且自洽的。
- 每个限界上下文内都应该有自己的通用语言,确保团队在内部沟通时不会产生歧义。
确定领域模型
- 对每个限界上下文,分析并建模其中的核心业务概念。领域模型的建立需要与业务专家和开发人员紧密协作,确保它们能够反映出业务的需求和目标。
定义上下文关系
- 使用上下文映射定义不同限界上下文之间的关系,确定如何进行集成和数据交互。常见的关系包括共享内核、客户-供应商、反向控制等。
优化核心领域
- 确定并聚焦于核心领域,投入更多的精力来精细化建模和设计。核心领域的代码和模型通常会影响到系统的业务功能和竞争力,因此需要特别关注其质量和适应性。
设计支持子系统
- 在划分和设计支撑子系统时,确保它们不会引入不必要的复杂性,并能有效支持核心领域的实现。
# 4. 战略设计的优势
- 降低复杂性:通过将系统分解成多个限界上下文,每个上下文内聚焦于业务的一部分,从而避免了全局复杂性。
- 业务与技术对齐:战略设计强调与业务领域的紧密对接,确保技术实现能够清晰地反映业务需求。
- 增强可扩展性:良好的限界上下文划分能够使得系统在增长时更加可扩展,不会因为复杂度增加而导致系统崩溃。
- 提高团队协作效率:每个限界上下文内的团队能够更加专注于自己的任务,减少不必要的沟通成本。
# 5. 战略设计与战术设计的区别
- 战略设计:关注系统的高层次架构、各个子系统之间的关系以及如何划分系统的领域边界。它处理的是“系统设计”层面的问题。
- 战术设计:关注具体的领域模型实现、聚合根、实体、值对象、领域服务等细节问题。它处理的是“如何实现”层面的问题。
# 6. 示例
假设我们正在开发一个电商平台,战略设计的过程可能包括以下步骤:
- 限界上下文:划分为“用户管理”,“商品管理”,“订单管理”,“支付管理”四个上下文,每个上下文中有不同的领域模型。
- 核心领域:订单管理可能是核心领域,因为它直接与收入和客户体验相关。
- 上下文映射:订单管理和支付管理可能是“客户-供应商”关系,而商品管理和用户管理可能是“共享内核”关系。
- 通用语言:定义“订单”、“支付”、“商品”等概念,并确保所有开发人员和业务专家都使用一致的语言描述这些业务元素。
# 7. 总结
战略设计通过将复杂的系统划分为多个限界上下文,并明确这些上下文之间的关系和责任,帮助开发团队高效地处理系统复杂性。它提供了一种面向业务的设计思维,确保技术架构能够支持和优化业务目标的实现。
# BOUNDED CONTEXT
原文:
大型项目上会有多个模型共存,在很多情况下这没什么问题。不同的模型应用于不同的上下文中。
一个模型只在一个上下文中使用。这个上下文可以是代码的一个特定部分,也可以是某个特定团队的工作。如果模型是在一次头脑风暴会议中得到的,那么这个模型的上下文可能仅限于那次讨论。
为了解决多个模型的问题,我们需要明确地定义模型的范围——模型的范围是软件系统中一个有界的部分,这部分只应用一个模型,并尽可能使其保持统一。团队组织中必须一致遵守这个定义。
明确地定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。
每个对象的整个生命周期都由负责模型的团队来处理,包括对象的持久化。由于这个团队也控制着数据库模式,因此他们特意把对象—关系映射设计得简单直接。换言之,数据库模式是由新模型驱动的,因此在 BOUNDED CONTEXT 的边界之内。
边界只不过是一些特殊的位置。各个 BOUNDED CONTEXT 之间的关系需要我们仔细地处理。CONTEXT MAP 画出了上下文的范围,并给出了 CONTEXT 以及它们之间联系的总体视图,而几种模式定义了 CONTEXT 之间的各种关系的性质。CONTINUOUS INTEGRATION 的过程可以使模型在 BOUNDED CONTEXT 中保持统一。
将不同模型的元素组合到一起可能会引发两类问题:重复的概念和假同源。重复的概念是指两个模型元素(以及伴随的实现)实际上表示同一个概念。
假同源可能稍微少见一点,但它潜在的危害更大。它是指使用相同术语(或已实现的对象)的两个人认为他们是在谈论同一件事情,但实际上并不是这样。
个人理解:
大型系统中模型共存,并且在不同的上下文环境中有不同的意义,这必定导致各个模块团队对于模型得混淆,破环领域驱动设计的原则,通过不同的上下文限界区分,可以避免重复和假同源的问题。
# CONTINUOUS INTEGRATION
原文:
当很多人在同一个 BOUNDED CONTEXT 中工作时,模型很容易发生分裂。
有时开发人员没有完全理解其他人所创建的对象或交互的意图,就对它进行了修改,使其失去了原来的作用。有时他们没有意识到他们正在开发的概念已经在模型的另一个部分中实现了,从而导致了这些概念和行为(不正确的)重复。有时他们意识到了这些概念有其他的表示,但却因为担心破坏现有功能而不敢去改动它们,于是他们继续重复开发这些概念和功能。
CONTINUOUS INTEGRATION 是指把一个上下文中的所有工作足够频繁地合并到一起,并使它们保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。像领域驱动设计中的
其他方法一样,CONTINUOUS INTEGRATION 也有两个级别的操作:(1)模型概念的集成;(2)实现的集成。
团队成员之间通过经常沟通来保证概念的集成。团队必须对不断变化的模型形成一个共同的理解。有很多方法可以帮助做到这一点,但最基本的方法是对 UBIQUITOUS LANGUAGE 多加锤炼。同时,实际工件通过系统性的合并/构建/测试过程来集成,这样能够尽早暴露出模型的分裂问题。用来集成的过程有很多,大部分有效的过程都具备以下这些特征:
分步集成,采用可重现的合并/构建技术; 自动测试套件; 有一些规则,用来为那些尚未集成的改动设置一个相当小的生命期上限。
在 MODEL-DRIVEN DESIGN 中,概念集成为实现集成铺平了道路,而实现集成验证了模型的有效性和一致性,并暴露出模型的分裂问题。
建立一个把所有代码和其他实现工件频繁地合并到一起的过程,并通过自动化测试来快速查明模型的分裂问题。严格坚持使用 UBIQUITOUS LANGUAGE,以便在不同人的头脑中演变出不同的概念时,使所有人对模型都能达成一个共识。
不要在持续集成中做一些不必要的工作。CONTINUOUS INTEGRATION 只有在 BOUNDED CONTEXT 中才是重要的。相邻 CONTEXT 中的设计问题(包括转换)不必以同一个步调来处理
CONTINUOUS INTEGRATION 可以在任何单独的 BOUNDED CONTEXT 中使用,只要它的工作规模大到需要两个以上的人去完成就可以。它可以维护单一模型的完整性。当多个 BOUNDED CONTEXT 共存时,我们必须要确定它们的关系,并设计任何必需的接口。
个人理解:
大型系统必定存在功能迭代和维护,持续集成必然存在,我们需要在找个过程中,循序渐进的集成合并,确保领域模型在所有开发人员的共识性,用统一语言沟通,抓大放小,只考虑单一BOUNDED CONTEXT中的模型一致性和实现完整性,对于多个BOUNDED CONTEXT 我们只考虑关系和设计接口。
# CONTEXT MAP
原文:
只有一个 BOUNDED CONTEXT 并不能提供全局视图。其他模型的上下文可能仍不清楚而且还在不断变化。
其他团队中的人员并不是十分清楚 CONTEXT 的边界,他们会不知不觉地做出一些更改,从而使边界变得模糊或者使互连变得复杂。当不同的上下文必须互相连接时,它们可能会互相重叠。
BOUNDED CONTEXT 之间的代码重用是很危险的,应该避免。功能和数据的集成必须要通过转换去实现。通过定义不同上下文之间的关系,并在项目中创建一个所有模型上下文的全局视图,可以减少混乱。
CONTEXT MAP 位于项目管理和软件设计的重叠部分。
识别在项目中起作用的每个模型,并定义其 BOUNDED CONTEXT。这包括非面向对象子系统的隐含模型。为每个 BOUNDED CONTEXT 命名,并把名称添加到 UBIQUITOUS LANGUAGE 中。
在每个 BOUNDED CONTEXT 中,都将有一种一致的 UBIQUITOUS LANGUAGE 的“方言”。我们需要把 BOUNDED CONTEXT 的名称添加到该方言中,这样只要通过明确 CONTEXT 就可以清楚地讨论任意设计部分的模型。
不管 CONTEXT MAP 采用什么形式,它必须在所有项目人员之间共享,并被他们理解。它必须为每个 BOUNDED CONTEXT 提供一个明确的名称,而且必须阐明联系点和它们的本质。
个人理解:
# 测试 CONTEXT 的边界
原文:
对各个 BOUNDED CONTEXT 的联系点的测试特别重要。这些测试有助于解决转换时所存在的一些细微问题以及弥补边界沟通上存在的不足。测试充当了有用的早期报警系统,特别是在我们必须信赖那些模型细节却又无法控制它们时,它能让我们感到放心。
个人理解:
各个上下文的划分之后的关系需要明确。
# CONTEXT MAP 的组织和文档化
原文:
两个重点:
BOUNDED CONTEXT 应该有名称,以便可以讨论它们。这些名称应该被添加到团队的 UBIQUITOUS LANGUAGE 中。 每个人都应该知道边界在哪里,而且应该能够分辨出任何代码段的 CONTEXT,或任何情况的 CONTEXT。
无论是哪种情况,将 CONTEXT MAP 融入到讨论中都是至关重要的,前提是 CONTEXT 的名称要添加到 UBIQUITOUS LANGUAGE 中。不要说“George 团队的内容改变了,因此我们也需要改变那些与其进行交互的内容”,而应该说:“Transport Network 模型发生了改变,因此我们也需要修改 Booking 上下文的转换器。”
个人理解:
多个context map 之间的关系和名称需要在 UBIQUITOUS LANGUAGE 中明确,作为通识。
# BOUNDED CONTEXT 之间的关系
原文:
开发一个紧密集成产品的优秀团队可以部署一个大的、统一的模型。如果团队需要为不同的用户群提供服务,或者团队的协调能力有限,可能就需要采用 **SHARED KERNEL(共享内核)**或 **CUSTOMER/SUPPLIER(客户/供应商)**关系。有时仔细研究需求之后可能发现集成并不重要,而系统最好采用 **SEPARATE WAY(各行其道)**模式。当然,大多数项目都需要与遗留系统或外部系统进行一定程度的集成,这就需要使用 **OPEN HOSTSERVICE(开放主机服务)**或 ANTICORRUPTION LAYER(防护层)。
个人理解:
我们可以用一些作者建议的模式来区分模型关系:SHARED KERNEL(共享内核),CUSTOMER/SUPPLIER(客户/供应商),SEPARATE WAY(各行其道),OPEN HOSTSERVICE(开放主机服务),ANTICORRUPTION LAYER(防护层)。
# SHARED KERNEL
原文:
当功能集成受到局限,CONTINUOUS INTEGRATION 的开销可能会变得非常高。尤其是当团队的技能水平或行政组织不能保持持续集成,或者只有一个庞大的、笨拙的团队时,更容易发生这种情况。在这种情况下就要定义单独的 BOUNDED CONTEXT,并组织多个团队。
从领域模型中选出两个团队都同意共享的一个子集。当然,除了这个模型子集以外,还包括与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位,一个团队在没与另一个团队商量的情况下不应擅自更改它。
SHARED KERNEL(共享内核)不能像其他设计部分那样自由更改。在做决定时需要与另一个团队协商。共享内核中必须集成自动测试套件,因为修改共享内核时,必须要通过两个团队的所有测试。通常,团队先修改各自的共享内核副本,然后每隔一段时间与另一个团队的修改进行集成。例如,在每天(或更短的时间周期)进行 CONTINUOUS INTEGRATION 的团队中,可以每周进行一次内核的合并。不管代码集成是怎样安排的,两个团队越早讨论修改,效果就会越好。
SHARED KERNEL 通常是 CORE DOMAIN,或是一组 GENERIC SUBDOMAIN(通用子领域),也可能二者兼有(参见第 15 章),它可以是两个团队都需要的任何一部分模型。使用 SHARED KERNEL 的目的是减少重复(并不是消除重复,因为只有在一个 BOUNDED CONTEXT 中才能消除重复),并使两个子系统之间的集成变得相对容易一些。
个人理解:
在遇到持续集成的局限时,我们就可以采用共享内核,多个团队仔细沟通,同步一部分核心领域或者子领域作为公用领域,共同继承,共同使用,从而减少重复开发,提高集成效率。
# CUSTOMER/SUPPLIER DEVELOPMENT TEAM
原文:
我们常常会碰到这样的情况:一个子系统主要服务于另一个子系统;“下游”组件执行分析或其他功能,这些功能向“上游”组件反馈的信息非常少,所有依赖都是单向的。两个子系统通常服务于完全不同的用户群,其执行的任务也不同,在这种情况下使用不同的模型会很有帮助。工具集可能也不相同,因此无法共享程序代码。
在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。
当某个客户对供应商的业务至关重要时,不同公司的项目之间也会出现客户/供应商关系。下游团队也能制约上游团队,一个有影响力的客户所提出的要求对上游项目的成功非常重要,但这些要求也能破坏上游项目的开发。建立正式的需求响应过程对双方都有利,因为与内部 IT 关系相比,在这种外部关系中更难做出“成本/效益”的权衡。
这种模式有两个关键要素。
关系必须是客户与供应商的关系,其中客户的需求是至关重要的。由于下游团队并不是唯一的客户,因此不同客户的要求必须通过协商来平衡,但这些要求都是非常重要的。这种关系与那种经常出现的“穷亲威”关系相反,在后者的关系中,下游团队不得不乞求上游团队满足其需求。 必须有自动测试套件,使上游团队在修改代码时不必担心破坏下游团队的工作,并使下游团队能够专注于自己的工作,而不用总是密切关注上游团队的行动。
CUSTOMER/SUPPLIER TEAM 涉及的团队如果能在同一个部门中工作,最后会形成共同的目标,这样成功机会将更大一些,如果两个团队分属不同的公司,但实际上也具有这些角色,同样也容易成功。但是,当上游团队不愿意为下游团队提供服务时,情况就会完全不同……
个人理解:
开发过程中必定存在不通系统之间的交互,当存在上下游关系时,我们就可以用客户与供应商的关系模型,做好沟通维护好整个模型得完整性。只有当上下游团队的目标一致时,合作共赢的条件才存在。
# CONFORMIST
原文:
当两个具有上游/下游关系的团队不归同一个管理者指挥时,CUSTOMER/SUPPLIER TEAM 这样的合作模式就不会奏效。勉强应用这种模式会给下游团队带来麻烦。大公司可能会发生这种情况,其中两个团队在管理层次中相隔很远,或者两个团队的共同主管不关心它们之间的关系。当两个团队属于不同公司时,如果客户的业务对供应商不是非常重要,那么也会出现这种情况。或许供应商有很多小客户,或者供应商正在改变市场方向,而不再重视老客户。也可能是供应商的运营状况较差,或者已经倒闭。不管是什么原因,现实情况是下游团队只能靠自己了。
团队将无能为力。出于利他主义的考虑,上游开发人员可能会做出承诺,但他们可能不会履行承诺。下游团队出于良好的意愿会相信这些承诺,从而根据一些永远不会实现的特性来制定计划。下游项目只能被搁置,直到团队最终学会利用现有条件自力更生为止。下游团队不会得到根据他们的需求而量身定做的接口。
在这种情况下,有 3 种可能的解决途径。一种是完全放弃对上游的使用。做出这种选择时,应进行切实地评估,绝不要假定上游会满足下游的需求。有时我们会高估这种依赖性的价值,或是低估它的成本。如果下游团队决定切断这条链,他们将走上 **SEPARATE WAY(各行其道)**的道路(参见本章后面介绍的模式)。
有时,使用上游软件具有非常大的价值,因此必须保持这种依赖性(或者是行政决策规定团队不能改变这种依赖性)。在这种情况下,还有两种途径可供选择,选择哪一种取决于上游设计的质量和风格。如果上游的设计很难使用(可能是由于缺乏封装、使用了不恰当的抽象或者建模时使用了下游团队无法使用的范式),那么下游团队仍然需要开发自己的模型。他们将担负起开发转换层的全部责任,这个层可能会非常复杂(参见本章后面要介绍的 ANTICORRUPTION LAYER)。
当使用一个具有很大接口的现成组件时,一般应该遵循(CONFORM)该组件中隐含的模型。组件和你自己的应用程序显然是不同的 BOUNDED CONTEXT,因此根据团队组织和控制的不同,可能需要使用适配器来进行一点点格式转换,但模型一定要保持相同。否则,就应该质疑使用该组件的价值。如果它确实能够提供价值,那说明它的设计中已经消化吸收了一些知识。在该组件的应用范围内,它可能比你的理解要深入。你的模型大概会超出该组件的范围,而且这些超出部分将演化出你自己的概念。但在两者连接的地方,你的模型将是一个 CONFORMIT,遵从组件模型的领导。实际上,你将被带到一个更好的设计中。
如果这些折中不可接受,而上游的依赖又必不可少,那么还可以选择第二种方法。通过创建一个 ANTICORRUPTION LAYER 来尽可能把自己隔离开,这是一种实现转换映射的积极方法,后面将会讨论它。
CONFORMIST 模式类似于 SHARED KERNEL 模式。在这两种模式中,都有一个重叠的区域——在这个重叠区域内模型是相同的,此外还有你的模型所扩展的部分,以及另一个模型对你没有影响的部分。这两种模式之间的区别在于决策制定和开发过程不同。SHARED KERNEL 是两个高度协调的团队之间的合作模式,而 CONFORMIST 模式则是应对与一个对合作不感兴趣的团队进行集成。
个人理解:
CONFORMIST(顺从者) 模式,重点是当一个团队依赖于另一个团队(特别是上游团队)时,如果两者不在同一个管理层级或者上游团队对下游团队不够重视,可能会出现的问题以及应对策略。
如果上游团队没有很好地履行承诺或无法提供有用的支持,下游团队可以选择以下3种方式之一:
- 完全放弃对上游的依赖: 如果下游团队发现上游团队的支持已经不再有价值,或者成本太高,它们可以选择完全脱离上游团队的依赖,走自己的路(称为 SEPARATE WAY,各自为政)。
- 依赖上游,但自己承担责任: 如果上游的系统或软件仍然有很大的价值,且下游团队无法切断依赖关系,团队可能会选择 顺从 上游的模型,即遵循上游系统的设计和接口。但如果上游的设计难以使用,或与下游团队的需求不匹配,下游团队可能需要为此开发转换层,以将上游的模型适配到自己的需求中。
- 通过创建 ANTICORRUPTION LAYER(抗腐化层)来隔离: 如果必须依赖上游,但又不希望上游的设计影响自己的模型,下游团队可以创建 抗腐化层,通过这个层将上游模型的影响隔离开来,只在这个层次上做转换,从而避免上游系统的设计对自己系统的污染。
# ANTICORRUPTION LAYER
原文:
新系统几乎总是需要与遗留系统或其他系统进行集成,这些系统具有其自己的模型。当把参与集成的 BOUNDED CONTEXT 设计完善并且团队相互合作时,转换层可能很简单,甚至很优雅。但是,当边界那侧发生渗透时,转换层就要承担起更多的防护职责。
正确答案是不要全盘封杀与其他系统的集成。在我经历过的一些项目中,人们非常热衷于替换所有遗留系统,但由于工作量太大,这不可能立即完成。此外,与现有系统集成是一种有价值的重用形式。在大型项目中,一个子系统通常必须与其他独立开发的子系统连接。这些子系统将从不同角度反映问题领域。当基于不同模型的系统被组合到一起时,为了使新系统符合另一个系统的语义,新系统自己的模型可能会被破坏。即使另一个系统被设计得很好,它也不会与客户基于同一个模型。而且其他系统往往并不是设计得很好。
如果从一个系统中取出一些数据,然后在另一个系统中错误地解释了它,那么显然会发生错误,甚至会破坏数据库。尽管我们已经认识到这一点,这个问题仍然会“偷袭”我们,因为我们认为在系统之间转移的是原始数据,其含义是明确的,并且认为这些数据在两个系统中的含义肯定是相同的。这种假设常常是错误的。数据与每个系统的关联方式会使数据的含义出现细微但重要的差别。而且,即使原始数据元素确实具有完全相同的含义,但在原始数据这样低的层次上进行接口操作通常是错误的。这样的底层接口使另一个系统的模型丧失了解释数据以及约束其值和关系的能力,同时使新系统背负了解释原始数据的负担(而且并未使用这些数据自己的模型)。
我们需要在不同模型的关联部分之间建立转换机制,这样模型就不会被未经消化的外来模型元素所破坏。
创建一个隔离层,以便根据客户自己的领域模型来为客户提供相关功能。这个层通过另一个系统现有接口与其进行对话,而只需对那个系统作出很少的修改,甚至无需修改。在内部,这个层在两个模型之间进行必要的双向转换。
个人理解:
系统总会在不断的与其他系统的集成过程中,让模型的界限变得模糊,当系统之间数据的交互之间存在解释或者理解偏差就将导致系统的领域模型系统知识的破坏,并且集成方都认为自己正确合理的运用理解了领域模型,并且系统提供的接口不一定会直接提供对于基础设施层的数据库模型提供给对接方,而是一些指定业务的接口,为了达到接口的复用性,提供老的接口给对接方系统使用,从而夹杂着业务和基础数据的接口必定导致对接方理解偏差,并且导致对接方系统的接口约束不在纯粹,丧失了解释数据以及约束其值和关系的能力,同时使新系统背负了解释原始数据的负担,创建一个隔离层,在不同模型的关联部分之间建立转换机制不然不同系统的模型被破坏,边界不打破。
# 设计 ANTICORRUPTION LAYER 的接口
原文:
ANTICORRUPTION LAYER 的公共接口通常以一组 SERVICE 的形式出现,但偶尔也会采用 ENTITY 的形式。构建一个全新的层来负责两个系统之间的语义转换为我们提供了一个机会,它使我们能够重新对另一个系统的行为进行抽象,并按照与我们的模型一致的方式把服务和信息提供给我们的系统。在我们的模型中,把外部系统表示为一个单独的组件可能是没有意义的。最好是使用多个 SERVICE(或偶尔使用 ENTITY),其中每个 SERVICE 都使用我们的模型来履行一致的职责。
个人理解:
反腐层的实现形式可以service和ENTITY,其实我们在各层之间转换也是起到防腐层的作用。
# 实现 ANTICORRUPTION LAYER
原文:
对 ANTICORRUPTION LAYER 设计进行组织的一种方法是把它实现为 FACADE、ADAPTER(这两种模式来自[Gamma et al. 1995])和转换器的组合,外加两个系统之间进行对话所需的通信和传输机制。
FACADE 是子系统的一个可供替换的接口,它简化了客户访问,并使子系统更易于使用。由于我们非常清楚要使用另一个系统的哪些功能,因此可以创建 FACADE 来促进和简化对这些特性的访问,并把其他特性隐藏起来。FACADE 并不改变底层系统的模型。它应该严格按照另一个系统的模型来编写。否则会产生严重的后果:轻则导致转换职责蔓延到多个对象中,并加重 FACADE 的负担;重则创建出另一个模型,这个模型既不属于另一个系统,也不属于你自己的 BOUNDED CONTEXT。FACADE 应该属于另一个系统的 BOUNDED CONTEXT,它只是为了满足你的专门需要而呈现出的一个更友好的外观。
ADAPTER 是一个包装器,它允许客户使用另外一种协议,这种协议可以是行为实现者不理解的协议。当客户向适配器发送一条消息时,ADAPTER 把消息转换为一条在语义上等同的消息,并将其发送给“被适配者”(adaptee)。之后 ADAPTER 对响应消息进行转换,并将其发回。我在这里使用适配器(adapter)这个术语略微有点儿不严谨,因为[Gamma et al. 1995]一书中强调的是使包装后的对象符合客户所期望的标准接口,而我们选择的是被适配的接口,而且被适配者甚至可能不是一个对象。我们强调的是两个模型之间的转换,但我认为这与 ADAPTER 的意图是一致的。
个人理解:
两个 FACADE、ADAPTER模式可作为防腐层实现的具体方案,通过组合,策略转化达到解耦,增强拓展性的同时不破坏不同系统的模型概念,打破边界,导致领域模型偏见的出现。
# 一个关于防御的故事
原文:
任何集成都是有开销的,无论这种集成是单一 BOUNDED CONTEXT 中的完全 CONTINUOUS INTEGRATION,还是集成度较轻的 SHARED KERNEL 或 CUSTOMER/SUPPLIER DEVELOPERTEAM,或是单方面的 CONFORMIST 模式和防御型的 ANTICORRUPTION LAYER 模式。集成可能非常有价值,但它的代价也总是十分高昂的。我们应该确保在真正需要的地方进行集成。
个人理解:
权衡利弊,合理集成。
# SEPARATE WAY
原文:
除了在团队之间进行协调所需的常见开销以外,集成还迫使我们做出一些折中。可以满足某一特定需求的简单专用模型要为能够处理所有情况的更加抽象的模型让路。或许有些完全不同的技术能够轻而易举地提供某些特性,但它却难以集成。或许某个团队很难合作,使得其他团队在尝试与之合作时找不到行之有效的方法。
在很多情况下,集成不会提供明显的收益。如果两个功能部分并不需要互相调用对方的功能,或者这两个部分所使用的对象并不需要进行交互,或者在它们操作期间不共享数据,那么集成可能就是没有必要的(尽管可以通过一个转换层进行集成)。仅仅因为特性在用例中相关,并不一定意味着它们必须集成到一起。
个人理解:
SEPARATE WAY 模式 代表了一种“断开依赖、自立自强”的策略。 适用于当上游资源无法满足需求,而下游团队有能力独立开发解决方案时。虽然有一定成本,但通过这种模式,团队可以摆脱外部限制,实现更高的独立性和灵活性。
# OPEN HOST SERVICE
原文:
定义一个协议,把你的子系统作为一组 SERVICE 供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。
个人理解:
OPEN HOST SERVICE 是一种为合作优化的模式,目的是通过一个标准化接口,让系统对多个用户或团队开放,同时保持系统的独立性和简洁性。我的理解就是一个策略接口,返回抽象对象,通过各个请求参数返回不同的响应对象实体。
# PUBLISHED LANGUAGE
原文:
OPEN HOST SERVICE 使用一个标准化的协议来支持多方集成。它使用一个领域模型来在各系统间进行交换,尽管这些系统的内部可能并不使用该模型。这里我们可以更进一步——发布这种语言,或找到一种已经公开发布的语言。我这里所说的发布仅仅是指该语言已经可以供那些对它感兴趣的群体使用,而且已经被充分文档化,兼容一些独立的解释。
个人理解:
PUBLISHED LANGUAGE 是一种帮助跨团队、跨系统合作的模式,通过制定统一的语言标准,让大家都能用一种“共同语言”高效、安全地交流。我们系统之间的协议不具备统一所有系统的作用,但是我们可以采用大家公认的协议来做到统一理解模型,这套语言可以是:
- 一个通用的 数据格式(如 JSON、XML、Protobuf 等)。
- 一组明确的 接口协议(如 REST API、SOAP)。
- 一个具体的 领域语言(Domain-Specific Language, DSL)
# 大象”的统一
原文:
从很多方面来讲,部分—整体的统一可能不需要花费很多工作。至少集成的第一步只需弄清楚各个部分是如何相连的就够了。
尽管我们已经把部分合并成一个整体,但得到的模型还是很简陋的。它缺乏内聚性,也没有形成任何潜在领域的轮廓。在持续精化的过程中,新的理解可能会产生更深层的模型。新的应用程序需求也可能会促成更深层的模型。
模型集成的第二步是去掉各个模型中那些偶然或不正确的方面,并创建新的概念,每个部分都有其自己的属性以及与其他部分的明确关系。在很大程度上,成功的模型应该尽可能做到精简。
承认多个互相冲突的领域模型实际上正是面对现实的做法。通过明确定义每个模型都适用的上下文,可以维护每个模型的完整性,并清楚地看到要在两个模型之间创建的任何特殊接口的含义。盲人没办法看到整个大象,但只要他们承认各自的理解是不完整的,他们的问题就能得到解决。
个人理解:
集成最重要的是各个团体之间的模型的关系,我们不需要对不同模型追求极致的统一,整个模型统一是一个过程,在这个过程中我们可以不断地优化和更深层次的理解模型,并且领域模型的完整性需要各个团队的共同构建才能最终达成。只需要每个团队对自己负责的模块儿负责以及交流,最终达成领域驱动模型的完善。
# 选择你的模型上下文策略
原文:
在任何时候,绘制出 CONTEXT MAP 来反映当前状况都是很重要的。但是,一旦绘制好 CONTEXT MAP 之后,你很可能想要改变现状。现在,你可以开始有意识地选择 CONTEXT 的边界和关系。以下是一些指导原则。
# 团队决策或更高层决策
原文:
首先,团队必须决定在哪里定义 BOUNDED CONTEXT,以及它们之间有什么样的关系。这些决策必须由团队做出,或者至少传达给整个团队,并且被团队里的每个人理解。事实上,这样的决策通常需要与外部团队达成一致。按照本身价值来说,在决定是否扩展或分割 BOUNDED CONTEXT 时,应该权衡团队独立工作的价值以及能产生直接且丰富集成的价值,以这两种价值的成本—效益作为决策的依据。在实践中,团队之间的行政关系往往决定了系统的集成方式。由于汇报结构,有技术优势的统一可能无法实现。管理层所要求的合并可能并不实用。你不会总能得到你想要的东西,但你至少可以评估出这些决策的代价,并反映给管理层,以便采取相应的措施来减小代价。从一个现实的 CONTEXT MAP 开始,并根据实际情况来选择改变。
个人理解:
团队划分上下文的过程,就像设计城市中的街区:
- 每个街区可以独立发展(独立性)。
- 但主干道必须畅通,街区之间的协作不能割裂(集成性)。 如果城市管理者随意合并街区而不考虑交通和功能需求,整个城市可能会运转不畅。街区规划者(团队)需要把这种可能的影响反馈给管理者。
团队决策或更高层决策的核心在于:
- 技术设计与管理限制的权衡。 从实际情况出发,基于独立性与集成性的权衡做决策,同时与管理层沟通可能的代价和改进方案,以推动系统设计的优化和团队协作的提升。
# 置身上下文中
原文:
开发软件项目时,我们首先是对自己团队正在开发的那些部分感兴趣(“设计中的系统”),其次是对那些与我们交互的系统感兴趣。典型情况下,设计中的系统将被划分为一到两个 BOUNDED CONTEXT,开发团队的主力将在这些上下文中工作,或许还会有另外一到两个起支持作用的 CONTEXT。除此之外,就是这些 CONTEXT 与外部系统之间的关系。这是一种简单、典型的情况,能让你对可能会遇到的情形有一些粗略的了解。
实际上,我们正是自己所处理的主要 CONTEXT 的一部分,这会在我们的 CONTEXT MAP 中反映出来。只要我们知道自己存在偏好,并且在超出该 CONTEXT MAP 的应用边界时能够意识到已越界,那么就不会有什么问题。
个人理解:
这就像一家餐厅的厨房运作:
- 厨房内部分为不同的工作区域(比如切菜区、烹饪区),每个厨师专注于自己的区域(核心上下文)。
- 供应商(外部系统)提供食材,餐厅需要与他们沟通合作,但不负责他们的生产流程。
- 如果厨师过于关注供应商的问题,而忽略了自己的工作区域,可能会导致整个厨房运作效率降低。
在开发软件项目时,团队需要清晰地划分自己的工作范围(BOUNDED CONTEXT),并关注 CONTEXT MAP 中标示的核心和外部上下文关系。明确边界、专注主要职责,同时对越界风险保持警觉,才能更高效地完成系统设计和开发工作。
# 转换边界
原文:
在画出 BOUNDED CONTEXT 的边界时,有无数种情况,也有无数种选择。但权衡时所要考虑的通常是下面所列出的某些因素。 首选较大的 BOUNDED CONTEXT 当用一个统一模型来处理更多任务时,用户任务之间的流动更顺畅。 一个内聚模型比两个不同模型再加它们之间的映射更容易理解。 两个模型之间的转换可能会很难(有时甚至是不可能的)。 共享语言可以使团队沟通起来更清楚。 首选较小的 BOUNDED CONTEXT 开发人员之间的沟通开销减少了。 由于团队和代码规模较小,CONTINUOUS INTEGRATION 更容易了。 较大的上下文要求更加通用的抽象模型,而掌握所需技巧的人员会出现短缺。 不同的模型可以满足一些特殊需求,或者是能够把一些特殊用户群的专门术语和 UBIQUITOUS LANGUAGE 的专门术语包括进来。 在不同 BOUNDED CONTEXT 之间进行深度功能集成是不切实际的。在一个模型中,只有那些能够严格按照另一个模型来表述的部分才能够进行集成,而且,即便是这种级别的集成可能也需要付出相当大的工作量。当两个系统之间有一个很小的接口时,集成是有意义的。
个人理解:
在划定 BOUNDED CONTEXT 的边界时,可能遇到多种情况,需要根据实际需求和团队能力做出选择。这种选择通常可以分为两种倾向:优先选择较大的 BOUNDED CONTEXT 和 优先选择较小的 BOUNDED CONTEXT。
如何选择?
- 优先选择 较大的 BOUNDED CONTEXT,如果团队可以设计出一个覆盖所有需求的统一模型,且转换复杂度高、任务间强相关。
- 优先选择 较小的 BOUNDED CONTEXT,如果团队较大、需求多样化,且较小的模型能更好地满足需求。
最终,需要根据实际项目的规模、团队的能力和需求的复杂程度,在这两种倾向之间找到最佳平衡。
# 接受那些我们无法更改的事物:描述外部系统
原文:
最好从一些最简单的决策开始。一些子系统显然不在开发中的系统的任何 BOUNDED CONTEXT 中。一些无法立即淘汰的大型遗留系统和那些提供所需服务的外部系统就是这样的例子。我们很容易就能识别出这些系统,并把它们与你的设计隔离开。| 在做出假设时必须要保持谨慎。我们会很轻易地认为这些系统构成了其自己的 BOUNDED CONTEXT,但大多数外部系统只是勉强满足定义。首先,定义 BOUNDED CONTEXT 的目的是把模型统一在特定边界之内。你可能负责遗留系统的维护,在这种情况下,可以明确地声明这一目的,或者也可以很好地协调遗留团队来执行非正式的 CONTINUOUS INTEGRATION,但不要认为遗留团队的配合是理所当然的事情。仔细检查,如果开发工作集成得不好,一定要特别小心。在这样的系统中,不同部分之间出现语义矛盾是很平常的事情。
个人理解:
在设计 BOUNDED CONTEXT 时,接受无法改变的外部系统是不可避免的,但我们需要谨慎处理与这些系统的集成。通过有效的隔离和转换机制,可以减少外部系统带来的复杂性和潜在问题。
# Relationships with the External Systems
原文:
可以应用 3 种模式。首先,
可以考虑 SEPARATE WAY 模式。
如果集成确实非常重要,可以在两种极端的模式之中进行选择:CONFORMIST 模式或 ANTICORRUPTION LAYER 模式。
当正在设计的系统功能并不仅仅是扩展现有系统时,而且你与另一个系统的接口很小,或者另一个系统的设计非常糟糕,那么实际上你会希望使用自己的 BOUNDED CONTEXT,这意味着需要构建一个转换层,甚至是一个 ANTICORRUPTION LAYER。
个人理解:
在与外部系统进行集成时,通常有三种主要的设计模式可以选择。具体使用哪一种模式取决于我们与外部系统的关系、集成的复杂度以及外部系统的设计质量。
- SEPARATE WAY 模式:完全隔离,避免集成。
- CONFORMIST 模式:适应外部系统的设计,紧密集成。
- ANTICORRUPTION LAYER 模式:通过构建转换层保护自己的系统模型,避免外部系统的负面影响。
# 设计中的系统
原文: 设计中的整个系统使用一个 BOUNDED CONTEXT。例如,当一个少于 10 人的团队正在开发高度相关的功能时,这可能就是一种很好的选择。 随着团队规模的增大,CONTINUOUS INTEGRATION 可能会变得困难起来(尽管我也曾看到过一些较大的团队仍能保持持续集成)。你可能希望采用 SHARED KERNEL 模式,并把几组相对独立的功能划分到不同的 BOUNDED CONTEXT 中,使得在每个 BOUNDED CONTEXT 中工作的人员少于 10 人。在这些 BOUNDED CONTEXT 中,如果有两个上下文之间的所有依赖都是单向的,就可以建成 CUSTOMER/SUPPLIER DEVELOPMENT TEAM。 你可能认识到两个团队的思想截然不同,以致他们的建模工作总是发生矛盾。可能他们需要从模型得到完全不同的东西,或者只是背景知识有某种不同,又或者是由于项目所采用的管理结构而引起的。如果这种矛盾的原因是你无法改变或不想改变的,那么可以让他们的模型采用 SEPARATE WAY 模式。在需要集成的地方,两个团队可以共同开发并维护一个转换层,把它作为唯一的 CONTINUOUS INTEGRATION 点。这与同外部系统的集成正好相反,在外部集成中,一般由 ANTICORRUPTION LAYER 来起调节作用,而且从另一端得不到太多的支持。 一般来说,每个 BOUNDED CONTEXT 对应一个团队。一个团队也可以维护多个 BOUNDED CONTEXT,但多个团队在一个上下文中工作却是比较难的(虽然并非不可能)。
个人理解:
- 小团队(少于 10 人的团队)适合一个统一的 BOUNDED CONTEXT。
- 大团队可以采用 SHARED KERNEL 模式,将系统划分为多个 BOUNDED CONTEXT(小团队)。
- 如果团队间的思想差异较大,可以使用 SEPARATE WAY 模式,将 BOUNDED CONTEXT 隔离开来。
- 通常一个 BOUNDED CONTEXT 对应一个团队,但多个团队可以协作开发和维护一个 BOUNDED CONTEXT。
# 用不同模型满足特殊需要
原文:
你可能决定通过不同的 BOUNDED CONTEXT 来满足这些特殊需要,除了转换层的 CONTINUOUS INTEGRATION 以外,让模型采用 SEPARATE WAY 模式。UBIQUITOUS LANGUAGE 的不同专用术语将围绕这些模型以及它们所基于的行话来发展。如果两种专用术语有很多重叠之处,那么 SHARED KERNEL 模式就可以满足特殊化要求,同时又能把转换成本减至最小。
个人理解:
SEPARATE WAY 模式 适用于需求差异较大、术语差异明显的情况,确保每个模型独立且专注于各自的领域。
SHARED KERNEL 模式 适用于术语和需求有较大重叠的情况,可以通过共享核心模型来减少转换成本,确保不同 BOUNDED CONTEXT 之间的协作和一致性。
# 部署
原文:
在复杂系统中,对打包和部署进行协调是一项繁琐的任务,这类任务总是要比看上去难得多。BOUNDED CONTEXT 策略的选择将影响部署。例如,当 CUSTOMER/SUPPLIER TEAM 部署新版本时,他们必须相互协调来发布经过共同测试的版本。在这些版本中,必须要进行代码和数据迁移。在分布式系统中,一种好的做法是把 CONTEXT 之间的所有转换层放在同一个进程中,这样就不会出现多个版本共存的情况。
当数据迁移可能很花时间或者分布式系统无法同步更新时,即使是单一 BOUNDED CONTEXT 中的组件部署也是很困难的,这会导致代码和数据有两个版本共存。
由于部署环境和技术存在不同,有很多技术因素需要考虑。但 BOUNDED CONTEXT 关系可以为我们指出重点问题。转换接口已经被标出。
绘制 CONTEXT 边界时应该反映出部署计划的可行性。当两个 CONTEXT 通过一个转换层连接时,要想更新其中的一个 CONTEXT,新的转换层需要为另一个 CONTEXT 提供相同的接口。SHARED KERNEL 需要进行更多的协调工作,不仅在开发中如此,而且在部署中也同样应该如此。SEPARATE WAY 模式可以使工作简单很多。
个人理解:
部署计划 需要考虑 BOUNDED CONTEXT 的关系和技术架构。绘制 CONTEXT 边界 时,要确保这些边界能够与部署需求匹配。
SEPARATE WAY 模式 适合于独立部署,减少了协调工作。
SHARED KERNEL 模式 在需要共享核心模型时,需要额外的协调,尤其在部署和更新时。
# 权衡
# 当项目正在进行时
# 转换
# 合并 CONTEXT:SEPARATE WAY →SHARED KERNEL
原文:
即使你的最终目标是完全合并成一个采用 CONTINUOUS INTEGRATION 的 CONTEXT,也应该先过渡到 SHARED KERNEL。
个人理解:
从 SEPARATE WAY 到 SHARED KERNEL 的过渡是一个逐步的过程,旨在优化系统间的协作和功能集成,减少重复工作,并统一模型和语言。然而,这种过渡也带来了更高的协调成本和可能的冲突,需要团队之间密切配合,才能实现长期的可维护性和系统的高效运作。
# 合并 CONTEXT:SHARED KERNEL→CONTINUOUS INTEGRATION
原文:
随着 SHARED KERNEL 的增长,把集成频率提高到每天一次,最后实现 CONTINUOUS INTEGRATION。 当 SHARED KERNEL 逐渐把先前两个 BOUNDED CONTEXT 的所有内容都包括进来的时候,你会发现要么形成了一个大的团队,要么形成了两个较小的团队,这两个较小的团队共享一个 CONTINUOUS INTEGRATION 的代码库,而且团队成员可以经常在两个团队之间来回流动。
个人理解:
从 SHARED KERNEL 到 CONTINUOUS INTEGRATION 的过渡是一种逐步整合和优化开发流程的过程。通过共享核心模型和不断提高集成频率,团队能够更高效地协作和交付功能。然而,这一过程中涉及到的技术协调、自动化测试和版本管理等挑战,需要团队高度重视并采取相应措施。最终,通过 CONTINUOUS INTEGRATION,团队可以更快速地响应需求变化,并提高整体的开发效率和质量。
# 逐步淘汰遗留系统
原文:
在任何一次迭代中:
确定遗留系统的哪个功能可以在一个迭代中被添加到某个新系统中; 确定需要在 ANTICORRUPTION LAYER 中添加的功能; 实现; 部署;
一旦最终进入运行阶段后,应该遵循如下步骤。
找出 ANTICORRUPTION LAYER 中那些不必要的部分,并去掉它们; 考虑删除遗留系统中目前未被使用的模块,虽然这种做法未必实际。有趣的是,遗留系统设计得越好,它就越容易被淘汰。而设计得不好的软件却很难一点儿一点儿地去除。这时,我们可以暂时忽略那些未使用的部分,直到将来剩余部分已经被淘汰,这时整个遗留系统就可以停止使用了。
个人理解:
逐步淘汰遗留系统的过程中,关键是通过迭代的方式将遗留系统的功能迁移到新系统中,并通过ANTICORRUPTION LAYER保护新系统不受旧系统的影响。每次迭代结束后,逐步清理不再需要的遗留系统部分,并在最终阶段彻底关闭遗留系统。通过这种方式,可以确保系统过渡的平稳性,减少对现有业务的影响,最终实现软件架构的现代化。
# OPEN HOST SERVICE→PUBLISHED LANGUAGE
原文:
我们已经通过一系列特定的协议与其他系统进行了集成,但随着需要访问的系统逐渐增多,维护负担也不断增加,或者交互变得很难理解。我们需要通过 PUBLISHED LANGUAGE 来规范系统之间的关系。
个人理解:
个人标准做的好就升级到公司标准,公司标准好就升级到行业标准,行业标准可以进一步晋升为全球标准。
- OPEN HOST SERVICE:
- 通过定义一组开放的、标准化的服务接口,使外部系统能够访问核心功能。
- 通常用于提供与外部系统的交互能力,确保灵活性和兼容性。
- PUBLISHED LANGUAGE:
- 定义一种通用的、公开的语言或数据格式,使系统能够以一致的方式与外部系统进行沟通。
- 通常通过文档化的协议和标准格式支持多系统集成。
通过合并,使两种模式的优势互补:
- 简化接口设计:通过 PUBLISHED LANGUAGE 直接替代原有的 OPEN HOST SERVICE 交互协议,减少复杂性。
- 统一通信语言:PUBLISHED LANGUAGE 既可以作为标准接口的数据格式,也可以作为系统间语义统一的工具。
- 提高维护性:减少对服务接口的独立开发,将核心业务功能与通信语言绑定。
# 第 15 章 精炼
原文:
领域模型的战略精炼包括以下部分:
帮助所有团队成员掌握系统的总体设计以及各部分如何协调工作; 找到一个具有适度规模的核心模型并把它添加到通用语言中,从而促进沟通; 指导重构; 专注于模型中最有价值的那部分; 指导外包、现成组件的使用以及任务委派。
# CORE DOMAIN
原文:
一个严峻的现实是我们不可能对所有设计部分进行同等的精化,而是必须分出优先级。为了使领域模型成为有价值的资产,必须整齐地梳理出模型的真正核心,并完全根据这个核心来创建应用程序的功能。但本来就稀缺的高水平开发人员往往会把工作重点放在技术基础设施上,或者只是去解决那些不需要专门领域知识就能理解的领域问题(这些问题都已经有了很好的定义)。
在制定项目规划的时候,必须把资源分配给模型和设计中最关键的部分。要想达到这个目的,在规划和开发期间每个人都必须识别和理解这些关键部分。
这些部分是应用程序的标志性部分,也是目标应用程序的核心诉求,它们构成了 CORE DOMAIN。CORE DOMAIN 是系统中最有价值的部分。
对模型进行提炼。找到 CORE DOMAIN 并提供一种易于区分的方法把它与那些起辅助作用的模型和代码分开。最有价值和最专业的概念要轮廓分明。尽量压缩 CORE DOMAIN。
让最有才能的人来开发 CORE DOMAIN,并据此要求进行相应的招聘。在 CORE DOMAIN 中努力开发能够确保实现系统蓝图的深层模型和柔性设计。仔细判断任何其他部分的投入,看它是否能够支持这个提炼出来的 CORE。
当必须在两个看起来都很有用的重构之间进行抉择时(由于时限的缘故),应该首选对 CORE DOMAIN 影响最大的那个重构。
个人理解:
技术高超的人员不应该把工作重心放在技术基础设施上,而应该聚焦于系统重要功能的精细化上,整理出模型的核心业务,根据核心模型来构建整个系统功能。核心领域就是核心业务,核心领域和其他辅助模型应该区分开来,核心业务要足够精炼,在重构上优先选择核心领域重构。
# 选择核心
原文:
一个应用程序的 CORE DOMAIN 在另一个应用程序中可能只是通用的支持组件。尽管如此,仍然可以在一个项目中(而且通常在一个公司中)定义一个一致的 CORE。像其他设计部分一样,人们对 CORE DOMAIN 的认识也会随着迭代而发展。开始时,一些特定关系可能显得不重要。而最初被认为是核心的对象可能逐渐被证明只是起支持作用。
个人理解:
不同系统的核心领域在不同的系统中可能只是一个通用支持组件,仅仅保持在核心系统中是未核心领域即可。
# 工作的分配
原文:
在项目团队中,技术能力最强的人员往往缺乏丰富的领域知识。这限制了他们的作用,并且更倾向于分派他们来开发一些支持组件,从而形成了一个恶性循环——知识的缺乏使他们远离了那些能够学到领域知识的工作。
打破这种恶性循环是很重要的,方法是建立一支由开发人员和一位或多位领域专家组成的联合团队,其中开发人员必须能力很强、能够长期稳定地工作并且对学习领域知识非常感兴趣,而领域专家则要掌握深厚的业务知识。如果你认真对待领域设计,那么它就是一项有趣且充满技术挑战的工作。你肯定也会找到持这种观点的开发人员。
自主开发的软件的最大价值来自于对 CORE DOMAIN 的完全控制。
个人理解:
技术能力强的人更应该去学习领域知识,而不是开发一下所谓的公共组件。需要能力强的人和领域专家共同完成领域知识沉淀,并且完成核心领域的设计和积累,并且我们也应该认识到人才的重要性,我们需要让掌握核心领域的人员长期稳定的留在公司团体中,需要一支稳定工作的团队,只要他们能够让我们的核心领域更为完善和精炼。所有自研软件的核心就是核心领域的完全掌控。
# 精炼的逐步提升
原文:
一份简单的 **DOMAIN VISION STATEMENT(领域愿景说明)**只需很少的投入,它传达了基本概念以及它们的价值。**HIGHLIGHTED CORE(突出核心)**可以增进沟通,并指导决策制定,这也只需对设计进行很少的改动甚至无需改动。
更积极的精炼方法是通过重构和重新打包显式地分离出 GENERIC SUBDOMAIN,然后单独进行处理。在使用 COHESIVE MECHANISM 的同时,也要保持设计的通用性、易懂性和柔性,这两个方面可以结合起来。只有除去了这些细枝末节,才能把 CORE 剥离出来。
重新打包出一个 SEGREGATED CORE(分离的核心),可以使这个 CORE 清晰可见(即使在代码中也是如此),并且促进将来在 CORE 模型上的工作。
最富雄心的精炼是 ABSTRACT CORE(抽象内核),它用纯粹的形式表示了最基本的概念和关系(因此,需要对模型进行全面的重新组织和重构)。
首先,我们可以把模型中最普通的那些部分分离出去,它们就是 GENERIC SUBDOMAIN(通用子领域)。GENERIC SUBDOMAIN 与 CORE DOMAIN 形成鲜明的对比,使我们可以更清楚地理解它们各自的含义。
个人理解:
每种精炼方法都需要根据具体情况决定投入的时间和资源,但它们具有连续性:
- 从简单的愿景声明开始,逐步明确领域核心。
- 通过分离通用子域、增强核心的独立性,逐步精炼核心模型。
- 最终实现抽象核心,为领域模型的未来发展提供更强的支持。
# GENERIC SUBDOMAIN
个人理解:
GENERIC SUBDOMAIN 与 CORE DOMAIN 的区别
特性 | CORE DOMAIN | GENERIC SUBDOMAIN |
---|---|---|
领域核心性 | 是领域的核心竞争力 | 支持领域核心,但不是核心 |
开发优先级 | 高优先级,投入更多资源 | 中等优先级,可以权衡资源投入 |
复用性 | 仅在领域内适用,领域外价值有限 | 领域内外均有较高的复用价值 |
复杂性 | 高复杂性,涉及领域专家的深度参与 | 低至中等复杂性,设计较为标准化 |
外包可能性 | 一般不适合外包 | 可以考虑外包或采购第三方解决方案 |
GENERIC SUBDOMAIN 是领域设计中的辅助部分,旨在提供通用功能,减轻核心子域的负担。通过合理的分离、标准化和复用,通用子域不仅能提高开发效率,还能确保系统的稳定性和灵活性,从而为核心业务提供强有力的支持。
# 通用不等于可重用
原文:
注意,虽然我一直在强调这些子领域的通用性,但我并没有提代码的可重用性。现成的解决方案可能适用于某种特殊情况,也可能不适用,但假设你要自己实现代码(内部实现或外包出去),那么不要特别关注代码的可重用性。因为那样做会违反精炼的基本动机——我们应该尽可能把大部分精力投入到 CORE DOMAIN 工作中,而只在必要的时候才在支持性的 GENERIC SUBDOMAIN 中投入工作。
重用确实会发生,但不一定总是代码重用。模型重用通常是更高级的重用,例如,当使用公开发布的设计或模型的时候就是如此。如果你必须创建自己的模型,那么它在以后的相关项目中可能很有价值。但是,虽然这样的模型概念可能适用于很多情况,我们也不必把它开发成“万能的”模型。我们只要把业务所需的那部分建模出来并实现即可。
尽管我们很少需要考虑设计的可重用性,但通用子领域的设计必须严格地限定在通用概念的范围之内。如果把行业专用的模型元素引入到通用子领域中,会产生两个后果。第一,它会妨碍将来的开发。虽然现在我们只需要子领域模型的一小部分,但我们的需求会不断增加。如果把任何不属于子领域概念的部分引入到设计中,那么再想灵活地扩展系统就很难了,除非完全重建原来的部分并重新设计使用该部分的其他模块。
第二,也是更重要的,这些行业专用的概念要么属于 CORE DOMAIN,要么属于它们自己的更专业的子领域,而且这些专业的模型比通用子领域更有价值。
个人理解:
通用性
- 指子域在业务场景中的适用范围较广,例如身份认证、日志管理等功能,能够支持多个领域模型。
- 通用性强调其概念、功能和作用能够满足领域内的广泛需求。
可重用性
- 特指代码或实现能够在多个上下文中重复使用。
- 可重用性更多关注于技术实现,而非业务需求。
通用不等于可重用。设计 GENERIC SUBDOMAIN 时,关键是满足领域的通用需求,而不是追求代码或实现上的全面复用。通过限制范围、专注当前需求和优化资源分配,可以在保证通用性和灵活性的同时,最大程度地支持核心业务的精炼工作。
# 项目风险管理
原文:
项目面临着两方面的风险,有些项目的技术风险更大,有些项目则是领域建模的风险更大一些。端到端的系统是实际系统中最困难部分的“雏形”——它控制风险的能力也仅限于此。当使用这种雏形时,我们很容易低估领域建模的风险。这种风险包括未预料到存在复杂性、与业务专家的交流不够充分,或者开发人员的关键技能存在欠缺等。
个人理解:
两种主要风险类型
- 技术风险
- 与技术选型、实现方法及技术栈相关。
- 常见问题:
- 新技术的不成熟性。
- 系统架构的复杂性。
- 缺乏对目标技术的充分了解。
- 领域建模风险
- 涉及对业务领域的理解与建模。
- 常见问题:
- 未预料到领域的复杂性。
- 与业务专家沟通不足,导致需求理解偏差。
- 开发人员缺乏领域建模的关键技能。
项目风险管理的核心在于平衡技术和领域建模两方面的挑战。通过识别、评估、缓解和监控风险,可以更有效地控制项目的不确定性,最终实现高质量交付。
# DOMAIN VISION STATEMENT
原文:
DOMAIN VISION STATEMENT 就是模仿这类文档创建的,但它关注的重点是领域模型的本质,以及如何为企业带来价值。在项目开发的所有阶段,管理层和技术人员都可以直接用领域愿景说明来指导资源分配、建模选择和团队成员的培训。如果领域模型为多个群体提供服务,那么此文档还能够显示出他们的利益是如何均衡的。
写一份 CORE DOMAIN 的简短描述(大约一页纸)以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方利益的。这份描述要尽量精简。尽早把它写出来,随着新的理解随时修改它。
DOMAIN VISION STATEMENT 可以用作一个指南,它帮助开发团队在精炼模型和代码的过程中保持统一的方向。团队中的非技术成员、管理层甚至是客户也都可以共享领域愿景说明(当然,包含专有信息的情况除外)。
个人理解:
DOMAIN VISION STATEMENT(领域愿景说明)是一份专注于领域模型本质及其为企业创造价值的简要文档。它能够在项目开发的各阶段指导资源分配、建模选择以及团队培训。尤其在涉及多个利益相关群体时,这份文档还能够体现出如何在不同群体之间实现利益的均衡。
# HIGHLIGHTED CORE
个人理解:
**HIGHLIGHTED CORE(突出核心)**是一种聚焦于项目核心领域的策略。通过明确项目的核心领域并对其进行优先处理,HIGHLIGHTED CORE 能够提升团队在有限资源条件下的开发效率,并确保关键领域的高质量实现。
这一方法有助于团队:
- 明确资源分配的优先级。
- 在开发过程中保持一致的方向感。
- 将注意力集中在最能为项目创造价值的部分。
# 精炼文档
原文:
编写一个非常简短的文档(3 ~ 7 页,每页内容不必太多),用于描述 CORE DOMAIN 以及 CORE 元素之间的主要交互过程。
独立文档带来的所有常见风险也会在这里出现:
文档可能得不到维护;
文档可能没人阅读; 由于有多个信息来源,文档可能达不到简化复杂性的目的。
控制这些风险的最好方法是保持绝对的精简。剔除那些不重要的细节,只关注核心抽象以及它们的交互,这样文档的老化速度就会减慢,因为这个层次的模型通常更稳定。
精炼文档应该能够被团队中的非技术人员理解。把它当作一个共享的视图,描述每个人都应该知道的东西,而且可以把它作为团队所有成员研究模型和代码的一个起点。
# 标明 CORE
# 把精炼文档作为过程工具
# COHESIVE MECHANISM
个人理解:
COHESIVE MECHANISM(内聚机制) 是指设计系统中的独立模块或组件,这些模块高度内聚,专注于完成某个特定功能或解决特定问题。它们封装了复杂的逻辑,提供通用的解决方案,并在系统中作为独立的机制运行。
通过这种方法,复杂问题被集中管理,重复逻辑被消除,从而提高了代码的复用性、可维护性以及整体开发效率。
# GENERIC SUBDOMAIN 与 COHESIVE MECHANISM 的比较
原文:
GENERIC SUBDOMAIN 与 COHESIVE MECHANISM 的动机是相同的——都是为 CORE DOMAIN 减负。区别在于二者所承担的职责的性质不同。GENERIC SUBDOMAIN 是以描述性的模型作为基础的,它用这个模型表示出团队会如何看待领域的某个方面。在这一点上它与 CORE DOMAIN 没什么区别,只是重要性和专门程度较低而已。COHESIVE MECHANISM 并不表示领域,它的目的是解决描述性模型所提出来的一些复杂的计算问题。
模型提出问题,COHESIVE MECHANISM 解决问题。
个人理解:
功能性和设计目标
特征 | GENERIC SUBDOMAIN | COHESIVE MECHANISM |
---|---|---|
功能 | 提供通用服务或基础设施功能,支持多个业务领域 | 封装特定功能的复杂逻辑,专注于高内聚功能的实现 |
设计目标 | 提供可复用的解决方案,支持多个上下文 | 提高模块的独立性、可维护性和功能的高内聚性 |
关注点 | 解决跨领域的共性问题,尽可能减少重复劳动 | 解决单一功能的复杂性,封装特定领域的复杂逻辑 |
适用场景
场景 | GENERIC SUBDOMAIN | COHESIVE MECHANISM |
---|---|---|
通用服务 | 提供基础设施级别的服务,如身份验证、消息队列等 | 提供独立的服务功能,如日志记录、数据加密等 |
跨领域功能 | 适用于多个领域或上下文中的共享功能 | 适用于在单一上下文或模块中需要高内聚逻辑的场景 |
系统复用 | 用于多个系统或子系统之间的功能共享 | 用于单一系统内部的功能模块化,提升代码复用性 |
设计原则
设计原则 | GENERIC SUBDOMAIN | COHESIVE MECHANISM |
---|---|---|
模块化 | 强调将不同子领域的功能独立分隔,确保每个子领域能独立工作 | 强调将功能逻辑高度内聚,模块化设计提高灵活性和可维护性 |
复用性 | 设计通用性较强,能够在多个上下文中进行复用 | 强调功能独立性,主要针对单一系统内的模块复用 |
抽象程度 | 更偏向于抽象和共性,避免与特定领域紧密绑定 | 专注于封装特定功能的复杂性,提供具体而高效的服务 |
两者的区别在于,通用子领域解决的是跨系统和跨业务的共性问题,而内聚机制则专注于单一系统内部的特定功能和复杂逻辑的处理。
# MECHANISM 是 CORE DOMAIN 一部分
原文:
我们几乎总是想要把 MECHANISM 从 CORE DOMAIN 中分离出去。例外的情况是 MECHANISM 本身就是专有的并且是软件的一项核心价值。
个人理解:
一般情况下,MECHANISM 应该从 CORE DOMAIN 中分离,以保证核心领域的简洁和灵活性。
特殊情况下,如果 MECHANISM 是核心竞争力的一部分,它应当成为 CORE DOMAIN 的一部分。
# 通过精炼得到声明式风格
原文:
COHESIVE MECHANISM 用途最大的地方是它通过一个 INTENTION-REVEALING INTERFACE 来提供访问,并且具有概念上一致的 ASSERTION 和 SIDE-EFFECT-FREE FUNCTION。利用这些 MECHANISM 和柔性设计,CORE DOMAIN 可以使用有意义的声明,而不必调用难懂的函数。但最不同寻常的回报来自于使 CORE DOMAIN 的一部分产生突破,得到一个深层模型,而且这部分核心领域本身成为了一种语言,可以灵活且精确地表达出最重要的应用场景。
深层模型往往与相对应的柔性设计一起产生。柔性设计变得成熟的时候,就可以提供一组易于理解的元素,我们可以明确地把它们组合到一起来完成复杂的任务,或表达复杂的信息,就像单词组成句子一样。此时,客户代码就可以采用声明式风格,而且更为精炼。
把 GENERIC SUBDOMAIN 提取出来可以减少混乱,而 COHESIVE MECHANISM 可以把复杂操作封装起来。这样可以得到一个更专注的模型,从而减少了那些对用户活动没什么价值的、分散注意力的方面。但我们不太可能为领域模型中所有非 CORE 元素安排一个适当的去处。SEGREGATED CORE(分离的核心)采用直接的方法从结构上把 CORE DOMAIN 划分出来。
个人理解:
通过精炼和封装,核心领域不仅能提供简洁的声明式接口,还能变得更加专注和清晰。COHESIVE MECHANISM帮助将复杂操作封装成易于理解的功能,从而为客户提供精炼而直观的代码结构。
# SEGREGATED CORE
原文:
对模型进行重构,把核心概念从支持性元素(包括定义得不清楚的那些元素)中分离出来,并增强 CORE 的内聚性,同时减少它与其他代码的耦合。把所有通用元素或支持性元素提取到其他对象中,并把这些对象放到其他的包中——即使这会把一些紧密耦合的元素分开。
通过重构得到 SEGREGATED CORE 的一般步骤如下所示。
识别出一个 CORE 子领域(可能是从精炼文档中得到的)。 把相关的类移到新的 MODULE 中,并根据与这些类有关的概念为模块命名。 对代码进行重构,把那些不直接表示概念的数据和功能分离出来。把分离出来的元素放到其他包的类(可以是新的类)中。尽量把它们与概念上相关的任务放在一起,但不要为了追求完美而浪费太长时间。把注意力放在提炼 CORE 子领域上,并且使 CORE 子领域对其他包的引用变得更明显且易于理解。 对新的 SEGREGATED CORE MODULE 进行重构,使其中的关系和交互变得更简单、表达得更清楚,并且最大限度地减少并澄清它与其他 MODULE 的关系(这将是一个持续进行的重构目标)。 对另一个 CORE 子领域重复这个过程,直到完成 SEGREGATED CORE 的工作。
个人理解:
SEGREGATED CORE(分离的核心)是领域驱动设计中优化和清晰化模型的一种方法。它的目标是将核心领域(CORE DOMAIN)从支持性元素中分离出来,增强核心的内聚性,同时减少与其他代码的耦合。这种方法通过重构来提取通用元素和支持性元素,并将它们独立到其他对象和包中,哪怕这会将一些原本紧密耦合的元素分开。
SEGREGATED CORE 的重构步骤:
- 识别核心子领域
- 确定需要分离的核心子领域,这可以通过精炼文档或现有的领域模型来帮助识别。
- 移除相关类并重新模块化
- 将与核心领域相关的类移到新的模块中,并为模块命名时使用与这些类和概念相关的名称。
- 重构代码
- 将那些不直接表示核心概念的数据和功能分离出来。将这些分离出来的元素放到新的包中,或创建新的类来管理它们。确保这些功能依然与概念上相关的任务进行分组,但不要过于追求完美,以免浪费时间。
- 关注 CORE 子领域的精炼
- 在重构过程中,保持关注核心子领域的精炼,确保这些领域与其他包的交互更加清晰和易于理解。
- 简化关系与交互
- 对新的 SEGREGATED CORE MODULE 进行进一步的重构,使其中的关系和交互更加简单和清晰,同时减少与其他模块的耦合。这个步骤应该是一个持续的目标,以确保系统中的模块尽可能低耦合并且易于理解。
- 重复过程
- 对其他核心子领域进行相同的重构,直到完成整个 SEGREGATED CORE 的任务。每个核心子领域都应尽量独立,并且最大程度地减少外部依赖。
# 创建 SEGREGATED CORE 的代价
个人理解:
创建SEGREGATED CORE的代价主要体现在以下几个方面:初期的重构成本、团队学习曲线、增加的维护成本、短期内的灵活性降低、潜在的设计错误风险和技术债务的管理等。虽然这项工作需要一定的投入,但如果能够从长远角度来看待,并妥善规划和执行,最终会提高系统的灵活性、可扩展性和可维护性,带来巨大的收益。
# 不断发展演变的团队决策
个人理解:
不断发展演变的团队决策是一个动态、协作、灵活的过程。通过透明的沟通、持续的反馈和灵活的迭代,团队能够更好地适应项目中的不确定性和变化。这不仅有助于提升决策的质量和执行力,还能增强团队成员的归属感和参与感,为项目的长期成功奠定基础。
# ABSTRACT CORE
原文:
通常,即便是 CORE DOMAIN 模型也会包含太多的细节,以至于它很难表达出整体视图。
我们处理大模型的方法通常是把它分解为足够小的子领域,以便能够掌握它们并把它们放到一些独立的 MODULE 中。这种简化式的打包风格通常是行之有效的,能够使一个复杂的模型变得易于管理。但有时创建独立的 MODULE 反而会使子领域之间的交互变得晦涩难懂,甚至变得更复杂。
当不同 MODULE 的子领域之间有大量交互时,要么需要在 MODULE 之间创建很多引用,这在很大程度上抵消了划分模块的价值;要么就必须间接地实现这些交互,而后者会使模型变得晦涩难懂。
我们不妨考虑采用横向切割而不是纵向切割的方式。多态性(polymorphism)允许我们忽略抽象类型实例的很多细节变化。如果 MODULE 之间的大部分交互都可以在多态接口这个层次上表达出来,那么就可以把这些类型重构到一个特定的 CORE MODULE 中。
把模型中最基本的概念识别出来,并分离到不同的类、抽象类或接口中。设计这个抽象模型,使之能够表达出重要组件之间的大部分交互。把这个完整的抽象模型放到它自己的 MODULE 中,而专用的、详细的实现类则留在由子领域定义的 MODULE 中。
大部分专用的类都将引用 ABSTRACT CORE MODULE,而不是其他专用的 MODULE。ABSTRACT CORE(抽象核心)提供了主要概念及其交互的简化视图。
个人理解:
ABSTRACT CORE 是领域驱动设计(DDD)中一种优化核心领域模型的技术。当 CORE DOMAIN(核心领域)模型变得过于复杂,细节繁多以至于难以掌握整体时,ABSTRACT CORE 通过提炼关键概念和简化交互,帮助团队构建一个清晰的核心视图。
传统方法的局限性:
- 将模型划分为多个小的子领域(独立 MODULE)通常有效,但在某些情况下会导致:
- 交互过多:模块之间的频繁引用使设计复杂化。
- 间接交互复杂:为避免直接依赖,采用间接交互方式,反而增加模型理解难度。
ABSTRACT CORE 的创新点: 通过横向切割(横跨子领域提取核心交互),而非纵向切割(按子领域分离),专注于多态性和抽象接口的使用,将核心概念集中到一个独立的 CORE MODULE 中。
# 深层模型精炼
个人理解:
深层模型精炼(Refining to a Deep Model)是领域驱动设计中的一个持续优化过程,旨在通过对子领域,尤其是核心领域(CORE DOMAIN)的深入理解和重构,将复杂的领域模型转化为更简单、更具有表现力的模型。
目标:
- 简化领域表达:用最少的模型元素表达最本质的领域逻辑。
- 解决关键问题:通过精炼,使模型能轻松应对核心业务中的复杂场景。
- 驱动项目成功:在 CORE DOMAIN 中的模型突破能够显著提升整个项目的价值。
精炼的特点
- 聚焦核心
- 持续重构
- 简单化表达
- 与柔性设计协同
精炼方法
- 深入理解领域
- 去除冗余与模糊
- 抽象与合成
- 模型与实际的对照
深层模型的特征
- 本质性
- 仅保留领域中最重要的部分,摒弃次要和无关内容。
- 例如:订单系统中,深层模型关注订单状态、生命周期等关键概念,而非次要的显示格式等。
- 组合性
- 深层模型的元素是可组合的。通过简单元素的灵活组合,可以轻松处理复杂问题。
- 例如:图形系统中,点、线、面的组合能够表示任何几何形状。
- 可扩展性
- 深层模型设计时考虑了未来扩展的需求,不会因新增场景而破坏已有结构。
- 显而易见
- 精炼后的深层模型具备很强的表达力,其含义一目了然,团队可以轻松理解。
# 选择重构目标
原文:
如果采用“哪儿痛治哪儿”这种重构策略,要观察一下根源问题是否涉及 CORE DOMAIN 或 CORE 与支持元素的关系。如果确实涉及,那么就要接受挑战,首先修复核心。 当可以自由选择重构的部分时,应首先集中精力把 CORE DOMAIN 更好地提取出来,完善对 CORE 的分离,并且把支持性的子领域提炼成通用子领域。
个人理解:
1. 哪儿痛治哪儿:从问题根源出发
- 当系统中存在明显的问题,需要根据痛点快速行动:
- 观察根源是否涉及 CORE DOMAIN 或 CORE 与支持元素的关系。
- 如果问题源于核心领域模型或核心与支持元素的耦合,优先集中力量修复这些核心问题。
- 接受挑战,解决核心问题,即使这需要花费更多时间和精力。
2. 自由选择重构的部分:聚焦核心
- 在没有迫切问题的情况下,可优先选择对系统未来发展最重要的模块进行重构:
- 提取 CORE DOMAIN
- 将领域的核心部分从复杂系统中清晰提取出来。
- 确保核心概念及其交互的设计清晰且易扩展。
- 完善 CORE 的分离
- 使用模块化设计,将核心领域与支持性逻辑分离。
- 减少耦合,使核心领域可以独立演化。
- 提炼支持性子领域
- 将非核心领域的功能模块化,使其具备通用性和复用性。
- 通过分离降低系统整体复杂度。
- 提取 CORE DOMAIN
# 第 16 章 大型结构
原文:
在一个大的系统中,如果因为缺少一种全局性的原则而使人们无法根据元素在模式(这些模式被应用于整个设计)中的角色来解释这些元素,那么开发人员就会陷入“只见树木,不见森林”的境地。我们需要理解各个部分在整体中的角色,而不必去深究细节。
“大型结构”是一种语言,人们可以用它来从大局上讨论和理解系统。它用一组高级概念或规则(或两者兼有)来为整个系统的设计建立一种模式。这种组织原则既能指导设计,又能帮助理解设计。另外,它还能够协调不同人员的工作,因为它提供了共享的整体视图,让人们知道各个部分在整体中的角色。
设计一种应用于整个系统的规则(或角色和关系)模式,使人们可以通过它在一定程度上了解各个部分在整体中所处的位置(即使是在不知道各个部分的详细职责的情况下)。
这种结构可以被限制在一个 BOUNDED CONTEXT 中,但通常情况下它会跨越多个 BOUNDED CONTEXT,并通过提供一种概念组织把项目涉及的所有团队和子系统紧密结合到一起。好的结构可以帮助人们深入地理解模型,还能够对精炼起到补充作用。
当团队规模较小而且模型也不太复杂时,只需将模型分解为合理命名的 MODULE,再进行一定程度的精炼,然后在开发人员之间进行非正式的协调,以上这些就足以使模型保持良好的组织结构了。
个人理解:
在开发大型系统时,缺乏全局性原则可能导致开发人员无法理解各部分在整体设计中的角色,从而陷入“只见树木,不见森林”的困境。大型结构(Large-Scale Structure)提供了一种语言,帮助开发团队在高层次上讨论、理解和组织系统设计。
实施大型结构的步骤
1. 确定全局规则和模式
- 定义系统设计的高级规则,例如模块划分、接口设计、依赖关系。
- 确定各模块的角色和职责,并建立它们之间的清晰关系。
2. 设计模块化结构
- 分解系统为多个 MODULE。
- 确保模块命名清晰且职责单一,便于团队成员快速理解。
3. 建立跨 BOUNDED CONTEXT 的概念组织
- 设计统一的概念框架,使跨团队协作更加顺畅。
- 提供跨子系统的共享视图,消除团队之间的理解差异。
4. 持续精炼模型
- 持续分析和优化模型的结构,逐步将复杂性转化为简单性。
- 通过引入新的规则或模式,进一步完善整体设计。
5. 定期共享全局视图
- 在团队之间定期分享大型结构的最新视图,确保全员对系统有一致理解。
- 使用可视化工具(如架构图)增强沟通效率。
# EVOLVING ORDER
个人理解:
在软件开发中,系统的复杂性和不确定性会随着时间的推移逐渐显现。固定的设计可能无法适应持续变化的业务需求,而在这种动态环境下,秩序需要随着领域知识的深入和团队协作的推进逐步演化。EVOLVING ORDER 强调了一种通过持续精炼和灵活调整,逐步建立和优化系统结构的过程。
# SYSTEM METAPHOR
原文:
SYSTEM METAPHOR(系统隐喻)是一种松散的、易于理解的大型结构,它与对象范式是协调的。由于系统隐喻只是对领域的一种类比,因此不同模型可以用近似的方式来与它关联,这使得人们能够在多个 BOUNDED CONTEXT 中使用系统隐喻,从而有助于协调各个 BOUNDED CONTEXT 之间的工作。
个人理解:
SYSTEM METAPHOR 是一种简单、易懂的大型结构设计方式,用于帮助团队理解和协调复杂系统的构建。它通过引入一种贴近领域的类比,将抽象的技术实现转化为直观的概念框架,从而简化沟通和决策。
# RESPONSIBILITY LAYER
# KNOWLEDGE LEVEL
原文:
KNOWLEDGE LEVEL 是 REFLECTION(反射)模式在领域层中的一种应用,很多软件架构和技术基础设施中都使用了它,[Buschmann et al. 1996])中给出了详尽介绍。REFLECTION 模式能够使软件具有“自我感知”的特性,并使所选中的结构和行为可以接受调整和修改,从而满足变化需要。
# PLUGGABLE COMPONENT FRAMEWORK
# 结构应该有一种什么样的约束
# 通过重构得到更适当的结构
# 最小化
# 沟通和自律
# 通过重构得到柔性设计
# 通过精炼可以减轻负担
# 第 17 章 领域驱动设计的综合运用
# 把大型结构与 BOUNDED CONTEXT 结合起来使用
# 将大型结构与精炼结合起来使用
# 首先评估
# 由谁制定策略
# 从应用程序开发自动得出的结构
# 以客户为中心的架构团队
# 制定战略设计决策的 6 个要点
原文:
决策必须传达到整个团队
决策过程必须收集反馈意见
计划必须允许演变
架构团队不必把所有最好、最聪明的人员都吸收进来
战略设计需要遵守简约和谦逊的原则
对象的职责要专一,而开发人员应该是多面手
个人理解:
制定战略设计决策的 6 个要点
决策必须传达到整个团队
战略设计决策需要在团队内部透明,确保每个人都清楚决策的内容及其影响。
实践方法
:
- 通过文档、会议或培训将决策清晰传达。
- 定期分享更新,解答团队疑问。
决策过程必须收集反馈意见
在做出关键设计决策前,应广泛听取团队成员的意见,特别是那些直接参与开发或对领域有深刻理解的人。
实践方法
:
- 组织讨论会或头脑风暴。
- 使用原型或模型演示以验证想法。
计划必须允许演变
设计不是一成不变的,战略需要适应不断变化的需求和技术环境。
实践方法
:
- 采用迭代式设计方法,允许在实践中逐步调整。
- 留出余地,为未来的扩展和优化做好准备。
架构团队不必把所有最好、最聪明的人员都吸收进来
架构团队的职责是制定方向,而不是独占所有资源。团队中的其他成员同样可以贡献关键想法和执行能力。
实践方法
:
- 架构团队专注于设计策略和协调,不干预具体实现。
- 鼓励团队成员在自己的领域发挥专业能力。
战略设计需要遵守简约和谦逊的原则
设计应当简单明了,避免过度复杂化;同时,设计者应虚心听取意见,避免盲目追求完美或忽略反馈。
实践方法
:
- 在设计中优先考虑最小化复杂度。
- 通过定期回顾和复盘,不断优化设计思路。
对象的职责要专一,而开发人员应该是多面手
设计层面:对象(如类、模块)应该专注于单一职责,避免混淆或过度耦合。
人员层面:开发人员需要掌握多种技能,以适应不同的任务需求。
实践方法
:
- 使用 SRP(单一职责原则) 指导对象设计。
- 鼓励团队成员学习跨领域技能,培养 T 型人才。