美文网首页
领域驱动设计 —— 软件核心复杂性应对之道

领域驱动设计 —— 软件核心复杂性应对之道

作者: Zeppelin421 | 来源:发表于2023-10-08 17:25 被阅读0次

本书为作出设计决策提供了一个框架,并且为讨论领域设计提供了一个技术词汇库
领域驱动设计是一种思维方式,也是一组优先任务,它旨在加速那些必须处理复杂领域的软件项目的开发

一、领域模型

模型

  • 模型被用来描绘人们所关注的现实或想法的某个方面,是一种简化。
  • 它是对现实的解释:把与解决问题密切相关的方面抽象出来,而忽略无关的细节
  • 模型这种知识形式对知识进行了选择性的简化和有意的结构化。适当的模型可以使人理解信息的意义,并专注于问题

领域模型

  领域模型并非某种特殊的图,而是这种图所要传达的思想。它绝不单单是领域专家头脑中的知识,而是对这类知识严格地组织且有选择地抽象。图可以表示和传达一种模型,同样精心书写的代码或文字也能达到同样的目的。

  领域建模并不是尽可能建立一个符合“现实”的模型。即使是对具体、真实世界中的事物进行建模,所得到的模型也不过是对事物的一种模拟。它也不单单是为了实现某种目的而构造出来的软件机制。建模更像是制作电影--出于某种目的而概况地反映现实。例如:即使是一部纪录片也不会原封不动地展示真实生活。就如同电影制片人讲述故事或阐明观点时,他们会选择素材,并以一种特殊方式将他们呈现给观众。

模型在DDD中的作用

  • 模型和设计的核心互相影响
    模型与实现之间的紧密联系才能使模型变得有用,并确保我们在模型中所进行的分析能够转化为最终产品
  • 模型是团队所有成员使用的通用语言的中枢
    由于模型与实现之间的关联,开发人员可以使用该语言来讨论程序,也可以在无需翻译的情况下与领域专家进行沟通
  • 模型是浓缩的知识
    模型是团队一致认同的领域知识的组织方式和重要元素的区分方式。透过我们如何选择术语,分解概念以及将概念联系起来,模型记录了我们看待领域的方式。

软件的核心

  软件的核心是其为用户解决领域相关的问题的能力。
  当领域很复杂时,这是一项艰巨的任务,它要求高水平技术人员的共同努力。开发人员必须钻研领域以获取业务知识,他们必须磨砺其建模技巧,并精通领域设计。

Ubiquitous Language(通用语言)

想要创建一种灵活的、蕴含丰富知识的设计,需要一种通用的、共享的团队语言,以及对语言不断地试验。

语言鸿沟

  领域专家对软件开发的技术术语所知有限,但他们能熟练使用自己领域的术语;开发人员可能会用一些描述性的、功能性的术语来理解和讨论系统,而这些术语并不具备领域专家的语言所要表达的意思。由于双方语言上存在鸿沟,领域专家只能模糊地描述他们想要的东西;开发人员虽然努力去理解一个自己不熟悉的领域,但也只能形成模糊的认识。

  如果语言支离破碎,项目必将遭遇严重的问题。
  1、领域专家使用他们自己的术语,而技术团队所使用的语言则经过调整,以便从设计角度讨论领域。
  2、日常讨论所使用的术语与代码中使用的术语不一致,甚至同一个人在讲话和写东西时使用的语言也不一致,这将导致对领域的深刻表述常常稍纵即逝,根本无法记录到代码或文档中
  3、翻译使得沟通不畅,并削弱了知识消化
  4、任何一方的语言都不能成为公共语言,因为它们无法满足所有的需求

UBIQUITOUS LANGUAGE

  项目需要一种公共语言,这种语言要比所有语言的最小公分母健壮。领域模型可以成为这种公共语言的核心,同时将团队沟通与软件实现紧密联系到一起。该语言将存在于团队工作中的方方面面。

  UBIQUITOUS LANGUAGE的词汇包括类和主要操作的名称。语言中的术语有些用来讨论模型中已经明确的规则;有些则来自施加于模型上的高级组织原则(Bounded Contex、Core Domain、Generic Subdomain);有些常常应用于领域模型的模式名称。

  模型之间的关系成为所有语言都具有的组合规则。词和短语的意义反映了模型的语义。

  开发人员应该使用基于模型的语言来描述系统中的工作、任务和功能。这个模型应该为开发人员和领域专家提供一种用于互相交流的语言,而且领域专家还应该使用这种语言来讨论需求、开发计划和特性。语言使用得越普通,理解进行得就越顺畅。

  讨论系统时要结合模型。使用模型元素及其交互来大声描述场景,并且按照模型允许的方式将各种概念结合到一起。找到更简单的表达方式来讲出你要讲的话,然后将这些新的想法应用到图和代码中。

“如果我们向Routing Service提供出发地、目的地和到达时间,就可以查询货物的停靠地点,......将它们存到数据库中。”(含糊且偏重于技术)
“出发地、目的地......把它们都输入到Routing Service中,而后我们得到一个Itinerary,它包含我们所需的全部信息。”(更具体,但过于啰嗦)
Routing Service查找满足Route Specification的Itinerary。(简洁)

Model-Driven Design(模型驱动设计)

  许多复杂项目确实在尝试使用某种形式的领域模型,但是并没有把代码的编写与模型紧密联系起来。这些项目所设计的模型在项目初期可能用来做一些探索工作。这种模型完全脱离程序设计,是由不同的人员开发;它是对业务领域进行分析的结果,它在组织业务领域中的概念时,完全不去考虑在软件系统中将会起到的作用,这种模型被称之为分析模型。

  分析模型仅仅是理解工具,在创建分析模型时并没有考虑程序设计的问题,因此分析模型很有可能无法满足程序设计的需求。如果开发人员不得不重新对设计进行抽象,那么大部分的领域知识就会被丢弃。如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的,软件的正确性也值得怀疑。

  Model-Driven Design不再将分析模型和程序设计分离开,而是寻求一种能够满足这两方面需求的单一模型。模型和设计的绑定需要的是在分析和程序设计阶段都能发挥良好作用的模型。如果模型对于程序的实现来说显得不太实用时,我们必须重新设计它。而如果模型无法忠实地描述领域的关键概念,也必须重新设计它。

Hands-On Modeler(亲身实践的建模者)

管理层认为建模人员就应该只负责建模工作,编写代码是在浪费这种技能。开始项目进展得还算顺利,领域专家以及各团队的开发负责人共同工作,消化领域知识并提炼出一个不错的核心模型。但该模型却从来没有派上用场,原因有两个:
其一:模型的一些意图在其传递过程中丢失了。模型的整体效果受细节影响很大,这些细节问题并不是总能在UML图或者一般讨论中遇到的
其二:模型与程序设计实现及技术互相影响,而模型设计者无法直接获得这种反馈。

  如果编写代码的人员认为自己没必要对模型负责,或者不知道如何让模型为应用程序服务,那么这个模型就和程序没有任何关联。如果开发人员没有意识到改变代码就意味着改变模型,那么他们对程序的重构不但不会增强模型的作用,反而还会削弱它的效果。同样,如果建模人员不参与到程序实现的过程中,那么对程序实现的约束就没有切身的感受,即使有,也会很快忘记。

  Model-Driven Design的两个基本要素(即模型要支持有效的实现并抽象出关键的领域知识)已经失去了一个,最终模型将变得不再实用。

  如果分工阻断了设计人员与开发人员之间的协作,使他们无法传达实现Model-Driven Design的种种细节,那么经验丰富的设计人员则不能将自己的知识和技术传递给开发人员

  Hands-On Modeler并不意味着团队成员不能有自己的专业角色。任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程度地参与模型讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过Ubiquitous Language与接触代码的人及时交换关于模型的想法。

二、战术设计

Layered Architecture(分层架构)


  要想创建出能够处理复杂任务的程序,需要做到关注点分离——使设计中的每个部分都得到单独的关注。在分离的同时,也需要维持系统内部复杂的交互关系。

用户界面层(表示层) 负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统
应用层 定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道 应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度
领域层(模型层) 负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心
基础设施层 为上面各层提供通用的技术能力;为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件等等。基础设施层还能够通过架构框架来支持4个层次间的交互模式

  给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉基本的业务知识,并有效地使用这些知识。

银行转账示例

Entity(实体)

  一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标识线”,这条线跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个具有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标识可能会破坏数据。

  主要由标识定义的对象被称作为Entity。Entity有特殊的建模和设计思路。它们具有生命周期,这期间它们的形式和内容可能发生根本变化,但必须保持一种内在的连续性。为了有效地跟踪这些对象,必须定义它们的标识。它们的类定义、职责、属性和关联必须由其标识来决定,而不依赖于其所具有的属性。

  当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。使类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这种方式应该与其形式和历史无关。要格外注意哪些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作为每个对象生成唯一的结果,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。模型必须定义出“符合什么条件才算是相同的事物”

  体育场座位预定程序可能会将座位和观众当做Entity来处理。在分配座位时,每张票都有一个座位号,座位是Entity。其标识就是座位号,它在体育场中是唯一的。座位可能还有很多其他属性,如位置、视野是否开阔、价格等,但只有座位号才用于标识和区分座位
  如果活动采用入场券的方式,那么观众可以寻找任意的空座位来坐,这样就不需要对座位加以区分。这种情况下,只有座位总数才是重要的。尽管座位上仍然印有座位号,但软件已经不需要跟踪它们。

Value-Object(值对象)

  跟踪Entity的标识是非常重要的,但为其他对象也加上标识会影响系统性能并增加分析工作,而且会使模型变得混乱,因为所有对象看起来都是相同的。软件设计要时刻与复杂性做斗争。我们必须区别对待问题,但在真正需要的地方进行特殊处理。如果仅仅把这类对象当作没有标识的对象,那么就忽略了它们的工具价值或术语价值。事实上,这些对象有其自己的特征,对模型也有着自己的重要意义。这些是用来描述事物的对象。

  用于描述领域的某个方面而本身没有概念标识的对象称为Value-Object

  当我们只关心一个模型元素的属性时,应把它归类为 Value-Object。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。Value-Object应该是不可变的。不要为它分配任何标识,而且不要把它设计成像Entity那么复杂

  在一个邮购公司的软件中,需要用地址来核实信用卡并投递包裹。但如果一个人的室友也从同一家公司订购了货物,那么是否意识到他们住在同一个地方并不重要。因此地址是一个VALUEOBJECT。
  在一个用于安排投递路线的邮政服务软件中,国家可能被组织为一个由地区、城市、邮政区、街区以及最终的个人地址组成的层次结构。这些地址对象可以从它们在层次结构中的父对象获取邮政编码,而且,如果邮政服务决定重新划分邮政区,那么所有地址都将随之改变。在这里,地址是一个ENTITY。
  在电力运营公司的软件中,一个地址对应于公司线路和服务的一个目的地。如果几个室友各自打电话申请电力服务,公司需要知道他们其实是住在同一个地方。在这种情况下,地址是一个ENTITY。换种方式,模型可以将电力服务与“住处”关联起来,那么住处就是一个带有地址属性的ENTITY了,这时,地址就是一个VALUEOBJECT。

Service(服务)

  在某些情况下,最清楚、最实用的设计会包含一些特殊的操作(一些活动或动作,而不是事物),这些操作从概念上讲不属于任何对象。这些领域概念不适合被建模为对象。如果勉强把这些重要的领域功能归为Entity或ValueObject的职责,那么不是扭曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,就是 Service。

  Service是作为接口提供的一种操作,它在模型中是独立的,它不像Entity和ValueObject那样具有封装的状态。它强调的是与其他对象的关系。使用Service时应该谨慎,它们不应该替代Entity和ValueObject的所有行为。但是当一个操作实际上是一种重要的领域概念时,Service很自然就会成为Model-Driven Design中的一部分。

  好的Service有以下三个特征:
  1、与领域概念相关的操作,不是Entity或ValueObject的一个自然组成部分
  2、接口是根据领域模型的其他元素定义的
  3、操作是无状态的

  平常所讨论的大多数Service是纯技术的Service,它们属于基础设施层,这点要和领域中的Service进行区分。领域层的Service会与这些基础设施层Service进行协作。

例如,银行可能有一个用于向客户发送电子邮件的应用程序,当客户的账户余额小于一个特定的临界值时,这个程序就会向客户发送一封电子邮件。
领域层Service负责确定是否满足临界值,而基础设施层Service负责通知

如果银行应用程序可以把交易进行转换并导出到一个电子表格文件中以便进行分析,那么这个导出操作就是应用层Service。“文件格式”在银行领域中是没有意义,它不涉及业务规则
账户之间的转账功能属于领域层Service,因为它包含重要的业务规则(如处理响应的借方账户和贷方账户),而且“资金转账”是一个有意义的银行术语。在这种情况下,Service自己不会做太多的事情,而只是要求两个Account对象完成大部分工作。但如果将“转账”操作强加在Account对象上会很别扭,因为这个操作涉及两个账户和一些全局规则。

Module(模块)

  Module是一个传统的,比较成熟的设计元素。使用Module的主要原因是“认知超载”。Module提供了两种观察模型的方式,一是可以在Module中查看细节而不会被整个模型淹没,二是观察Module之间的关系而不考虑其内部细节。

  Module之间应该是低耦合的,而在Module的内部则是高内聚的。Module并不仅仅是代码的划分,同时也是概念的划分。在一个好的模型中,元素之间是要协同工作的,将具有相关职责的对象元素聚合到单一Module中,会极大地降低建模和设计的复杂性。

  Module是一种表达机制。Module的选择应该取决于被划分到模块中的对象的意义。当你将一些类放到Module中时,相当于告诉下一位看到你的设计的开发人员要把这些类放在一起考虑。如果说模型讲述了一个故事,那么Module就是这个故事的各个章节。模块的名称表达了其意义,当我们说“现在让我们讨论一下'客户'模块”,这就相当于为接下来的对象设定了上下文。

Aggregate(聚合)

假设从数据库中删除一个Person对象。这个人的姓名、出生日期和工作描述要一起被删除,但要如何处理地址呢?可能还有其他人住在同一地址。如果删除地址,那其他Person对象将会引用一个被删除的对象。如果保留地址,那么垃圾地址在数据库中会累积起来。

  在具有复杂关联的模型中,想要保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。然后过于谨慎的锁定机制又会导致多个用户之间毫无意义地互相干扰,从而使系统不可用。

  在任何具有持久化数据存储的系统中,对数据进行修改的事务必须要有范围,而且要有保持数据一致性的方式。尽管从表面上看这个问题是数据库事务方面的一个技术难题,但它的根源却是模型,归根结底是由于模型中缺乏明确定义的边界。从模型得到的解决方案将使模型更易于理解,并且使设计更易于沟通。当模型被修改时,它将引导对实现作出修改。

  Aggregate就是一组相关对象的集合,它将作为数据修改的单元。每个Aggregate都有一个根(root)和一个边界(boundary)。边界定义了Aggregate的内部都有什么。根则是Aggregate所包含的一个特定Entity。对Aggregate而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。除根以外的其他Entity都有本地标识,但这些标识只有在Aggregate内部才需要加以区别,因为外部对象除根Entity之外看不到其他对象。

  固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及到Aggregate成员之间的内部关系。而任何跨越Aggregate的规则将不要求每时每刻都保持最新状态。通过事件处理、批处理或其他更新机制,这些依赖会在一定的时间内得以解决。但在每个事务完成时,Aggregate内部所应用的固定规则必须得到满足。

为了实现这个概念上的Aggregate,需要对所有事务应用一组规则:

  • 根Entity具有全局标识,它最终负责检查固定规则
  • 根Entity具有全局标识。边界内的Entity具有本地标识,这些标识只在Aggregate内部才是唯一的。
  • Aggregate外部的对象不能引用除根Entity之外的任何内部对象。根Entity可以把对内部Entity的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个ValueObject的副本传递给另一个对象,而不必关心它发生什么变化,因为它只是一个Value,不再与Aggregate有任何关联
  • 作为上一条规则的推论,只有Aggregate的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现
  • Aggregate内部的对象可以保持对其他Aggregate根的引用
  • 删除操作必须一次删除Aggregate边界之内的所有对象。
  • 当提交对Aggregate边界内部的任何对象的修改时,整个Aggregate的所有固定规则都必须被满足。

Factory(工厂)

  当创建一个对象或创建整个Aggregate时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用Factory进行封装。

  对象的创建本身可以是一个主要操作,但被创建的对象并不合适承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或Aggregate的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。

  因此,应该将创建复杂对象的实例和Aggregate的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计中的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建Aggregate时要把它作为一个整体,并确保它满足固定规则。

Repository(仓库)

  无论要用对象执行什么操作,都需要保持一个对它的引用。如何获得这个引用呢?一种方法是创建对象,因为创建操作将返回对新对象的引用。第二种方法是遍历关联,以一个已知对象作为起点,并向它请求一个关联的对象。这样的操作在任何面向对象的程序中都会大量用到,而且对象之间的这些链接使对象模型具有更强的表达能力。

  人们将大部分对象存储在关系数据库中,这种存储技术使人们自然而然地使用第三种获取引用的方式-----基于对象的属性,执行查询来找到对象;或者是找到对象的组成部分,然后重建它。

  客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过Aggregate的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而Entity和ValueObject则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性就会使客户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。

从技术的观点来看,检索已存储对象实际上属于创建对象的范畴,因为从数据库中检索出来的数据要被用来组装新的对象。实际上,由于需要经常编写这样的代码,我们对此形成了根深蒂固的观念。但从概念上讲,对象检索发生在Entity生命周期的中间。不能只是因为我们将对象保存在数据库中,而后把它检索出来,这个就代表一个新的对象。为了记住这个区别,我们把使用已存储的数据创建实例的过程称为重建。
假设开发人员构造了一个SQL查询,并将它传递给基础设施层中的某个查询服务,然后再根据得到的表行数据的结果集提取出所需信息,最后将这些信息传递给构造函数或Factory。开发人员执行这一连串操作的时候,早已不再把模型当作重点了。我们很自然地会把对象看作容器来放置查询出来的数据,这样整个设计就转向了数据处理风格。

  Repository是一个简单的概念框架,它可用来封装这些解决方案,并将我们的注意力重新拉回到模型上。Repository将某种类型的所有对象表示为一个概念集合。它的行为类似于集合,只是具有更复杂的查询功能。在添加或删除相应类型的对象时,Repository的后台机制负责将对象添加到数据库中,或从数据库中删除对象。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对Aggregate根的整个生命周期的全程访问。

  客户使用查询方法向Repository请求对象,这些查询方法根据客户所指定的条件来挑选对象。Repository检索被请求的对象,并封装数据库查询和元数据映射机制。Repository可以根据客户所要求的各种条件来挑选对象。它们也可以汇总信息,如有多少个实例满足查询条件。

Repository有很多优点:

  • 它们为客户提供了一个简单的模型,可用来获取持久化对象并管理它们的生命周期
  • 它们使应用程序和领域设计与持久化技术(多种数据库策略甚至多个数据源)解耦
  • 它们体现了有关对象访问的设计决策
  • 可以很容易将它们替换为“哑实现”,以便在测试中使用

尽管Repository和Factory本身并不是来源于领域,但它们在领域设计中扮演着重要的角色。使用Aggregate进行建模,并在设计中结合使用Factory和Repository,这样我们就能够在模型对象的整个生命周期中,以有意义的单元,系统地操纵它们。Aggregate可以划分出一个范围,这个范围内的模型元素在生命周期各个阶段都应该维护其固有规则。Factory和Repository在Aggregate基础上进行操作,将特定生命周期转换的复杂性封装起来。

三、战略设计

Bounded Context(限界上下文)

  任何大型项目都会存在多个模型。而当基于不同模型的代码被组合到一起后,软件就会出现BUG,变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型不应该在哪个上下文中使用。

  一个模型只在一个上下文中使用。这个上下文可以是代码的一个特定部分,也可以是某个特定团队的工作。如果模型是在一次头脑风暴会议中得到的,那么这个模型的上下文可能仅限于那次讨论。明确定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式等)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。

  Bounded Context明确地限定了模型的应用范围,以便让团队成员对什么应该保持一致以及上下文之间如何关联有一个明确和共同的理解。在Context中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。在其他Context中,会使用其他模型,这些模型具有不同的术语、概念、规则和Ubiquitous Language的技术行话。通过划定明确的边界,可以使模型保存纯粹,因而在它所适用的Context中更有效。同时也避免了将注意力切换到其他Context时引起的混淆。

将不同模型的元素组合到一起可能会引发两类问题:
重复概念:
两个模型元素(以及伴随的实现)实际上表示同一个概念。每当这个概念的信息发生变化时,都必须更新两个地方。每次由于新知识导致一个对象被修改时,必须重新分析和修改另一个对象。如果不进行实际的重新分析,结果就会出现同一个概念的两个版本,它们遵循不同的规则,甚至有不同的数据。
假同源:
使用相同术语(或已实现的对象)的两个人认为他们是在讨论同一件事,但实际上并不是这样。假同源会导致开发团队互相干扰对方的代码,也可能导致数据库中含有奇怪的矛盾,还会引起团队沟通的混淆。
当发现这些问题时,团队必须要做出相应的决定。可能需要将模型重新整合为一体,并加强用来预防模型分裂的过程。

Continuous Integration(持续集成)

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

  Continuous Integration是指把一个上下文中的所有工作足够频繁地合并到一起,并使它们保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。Continuous Integration也有两个级别的操作:
  1、模型概念的集成:团队成员之间通过经常沟通来保证概念的集成。团队必须对不断变化的模型形成一个共同的理解。最基本的方法是对Ubiquitous Language多加锤炼
  2、实现的集成:大多数敏捷项目至少每天会把每位开发人员所做的修改合并进来。这个频率可以根据更改的步伐来调整,只要确保该间隔不会导致大量不兼容的工作产生即可。

  不要在持续集成中做一些不必要的工作。Continuous Integration只有在Bounded Context中才是重要的。相邻Context中的设计问题(包括转换)不必以同一个步调来处理。

Context Map(上下文图)

  只有一个Bounded Context并不能提供全局视图。其他团队中的人员并不是十分清楚Context的边界,他们会不知不觉地做出一些更改,从而使边界变得模糊或者使互连变得复杂。当不同的上下文必须互相连接时,它们可能会互相重叠。Bounded Context之间的代码重用是很危险的,应该避免。功能和数据的集成必须要通过转换去实现。通过定义不同上下文之间的关系,并在项目中创建一个所有模型上下文的全局视图,可以减少混乱。

  Context Map位于项目管理和软件设计的重叠部分。按照常规,往往按照团队组织的轮廓来划定边界。紧密协作的人会很自然地共享一个模型上下文。不同团队的人员将使用不同的上下文。大多数项目经理会本能地意识到这些因素,并围绕子系统大致把各个团队组织起来。

  Context Map无需拘泥于任何特定的文档格式。它必须在所有项目人员之间共享,并被他们理解。它必须为每个Bounded Context提供一个明确的名称,而且必须阐明联系点和它们的本质。

Shared Kernel(共享内核)

  当不同团队开发一些紧密相关的应用程序时,如果团队之间不进行协调,即使短时间内能够取得快速进展,但他们开发出的产品可能无法结合到一起。最后可能不得不耗费大量精力在转换层上,并且频繁地进行改动。不如一开始就使用Continuous Integration那么省心省力,同时这也造成重复工作,并且无法实现公共的Ubiquitous Language所带来的好处。

  在很多项目中,一些基本上独立工作的团队共享基础设施层。领域工作采用类似的方法也可以得到很好的效果。保持整个模型和代码完全同步的开销可能太高了,但从系统中仔细挑选出一部分并保持同步,就能以较小的代价获得较大的收益。

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

  Shared Kernel通常是Core Domain,或是一组Generic Subdomain,也可能二者兼有,它可以是两个团队都需要的任何一部分模型。使用Shared Kernel的目的是减少重复(并不是消除重复,只有在一个Bounded Context中才能消除重复),并使两个子系统之间的集成变得相对容易一些。

Customer/Supplier Development Team(客户/供应商开发团队)

  一个子系统主要服务于另一个子系统;“下游”组件执行分析或其他功能,这些功能向“上游”组件反馈的信息非常少,所有依赖都是单向的。两个子系统通常服务于完全不同的用户群,其执行的任务也不同,在这种情况下使用不同的模型会很有帮助。工具集可能也不相同,因此无法共享程序代码。上游和下游子系统很自然地分隔到两个BOUNDED CONTEXT中。如果两个组件需要不同的技能或者不同的工具集来实现时,更需要把它们隔离到不同的上下文中。转换很容易,因为只需要进行单向转换。但两个团队的行政组织关系可能会引起问题。

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

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

Conformist(追随者模式)

  当两个具有上游/下游关系的团队不归同一个管理者指挥时,Customer/Supplier Team这样的合作模式就不会奏效。当两个开发团队具有上/下游关系时,如果上游团队没有动力来满足下游团队的需求,那么下游团队将无能为力。出于利他主义的考虑,上游开发人员可能会做出承诺,但他们可能不会履行承诺。下游团队出于良好的意愿会相信这些承诺,从而根据一些永远不会实现的特性来制定计划。下游项目只能被搁置,直到团队最终学会利用现有条件自力更生为止。下游团队不会得到根据他们的需求而量身定做的接口。

有三种解决途径:

  • 完全放弃对上游的使用。如果下游决定切断这条链,他们将各行其道(Separate Way)
  • 保持这种依赖性
    • 上游的设计很难使用 下游团队仍然需要开发自己的模型。他们将负担起开发转换层的全部职责,这个层可能会非常复杂(Anticorruption Layer)
    • 上游设计的质量不是很差 通过严格遵从上游团队的模型,可以消除在Bounded Context之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择Conformist模式可以极大地简化集成。此外,这样还可以与供应商团队共享Ubiquitous Language。供应商处于统治地位,因此最好使沟通变容易。他们从利他主义的角度出发,会与你分享信息

Shared Kernel是两个高度协调的团队之间的合作模式,而Conformist则是应对与一个对合作不感兴趣的团队进行集成

Anticorruption Layer(防腐层)

  新系统几乎总是需要与遗留系统或其他系统进行集成,这些系统具有其自己的模型。当把参与集成的Bounded Context设计完善并且团队互相合作时,转换层可能很简单,甚至很优雅。但是,当边界那侧发生渗透时,转换层就要承担起更多的防护职责。

  当正在构建的新系统与另一个系统的接口差别很大时,为了克服连接两个模型而带来的困难,新模型所表达的意图可能会被完全改变,最终导致它被修改得像是另一个系统的模型了。遗留系统的模型通常很弱。即使对于那些模型开发得很好的例外情况,它们可能也不符合当前项目的需要。然而,集成遗留系统仍然具有很大的价值,而且有时还是绝对必要的。

替换所有遗留系统,工作量太大,不可能立即完成
当基于不同模型的系统被组合到一起时,为了使新系统符合另一个系统的语义,新系统自己的模型可能会被破坏
当通过接口与外部系统连接时,存在很多障碍。两个系统可能处于不同的平台上,或是使用了不同的协议。
从一个系统中取出数据,然后在另一个系统中解释它,很可能会发生错误,甚至会破坏数据库

  我们需要在不同模型的关联部分之间建立转换机制,这样模型就不会被未经消化的外来模型元素所破坏。创建一个隔离层,以便根据客户自己的领域模型来为客户提供相关功能。这个层通过另一个系统现有接口与其进行对话,而只需对那个系统作出很少的修改,甚至无需修改。在内部,这个层在两个模型之间进行必要的双向转换。

  Anticorruption Layer的公共接口通常以一组Service的形式出现,但偶尔也会采用Entity的形式。构建一个全新的层来负责两个系统之间的语义转换为我们提供了一个机会,它使我们能够重新对另一个系统的行为进行抽象,并按照我们的模型一致的方式把服务和信息提供给我们的系统。

  对Anticorruption Layer设计进行组织的一种方法是把它实现为Facade、Adapter:
  Facade:是子系统的一个可供替换的接口,它简化了客户访问,并使子系统更易于使用。Facade并不改变底层系统的模型,它应该严格按照另一个系统的模型来编写。Facade应该属于另一个系统的Bounded Context,它只是为了满足你的专门需要而呈现出的一个更友好的外观
  Adapter:是一个包装器,它允许客户使用另外一种协议,这种协议可以是行为实现者不理解的协议。当客户向适配器发送一条消息时,Adapter把消息转化为一条在语义上等同的消息,并将其发送给“被适配者”。之后Adapter对相应消息进行转换,并将其发回。

Separate Way(各行其道)

  如果两组功能之间的关系并非必不可少,那么二者完全可以彼此独立。

  集成总是代价高昂,而有时获益却很小。除了在团队之间进行协调所需的常见开销以外,集成还迫使我们做出一些折中。可以满足某一特定需求的简单专用模型要为能够处理所有情况的更加抽象的模型让路。或许有些完全不同的技术能够轻而易举地提供某些特性,但它却很难集成。或许某个团队很难合作,使得其他团队在尝试与之合作时找不到行之有效的方法。

  如果两个功能部分并不需要互相调用对方的功能,或者这两个部分所使用的对象并不需要进行交互,或者在它们操作期间不共享数据,那么集成可能就是没有必要的。

Open Host Service(开放主机服务)

  在Bounded Context中工作时,我们会为Context外部的每个需要集成的组件定义一个转换层。当集成是一次性的,这种为每个外部系统插入转换层的方法可以以最小的代价避免破坏模型。但当子系统要与很多系统集成时,可能就需要更灵活的方法了。

  当一个子系统必须与大量其他系统进行集成时,为每个集成都定制一个转换层可能会减慢团队的工作速度。需要维护的东西会越来越多,而且进行修改的时候担心的事情也会越来越多。如果一个子系统有某种内聚性,那么或许可以把它描述为一组Service,这组Service满足了其他子系统的公共需求。

  定义一个协议,把你的子系统作为一组Service供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。这样,其他子系统就变成了与 Open Host(开发主机)的模型相连接,而其他团队则必须学习Host团队所使用的专用术语。

Published Language(发布语言)

  两个Bounded Context之间的模型转换需要一种公共的语言。

  如果正在构建一个新系统,我们通常会认为新模型是最好的,因此只考虑把其他模型转换成新模型就可以了。如果是为了增强一系列旧系统并尝试集成它们,或者不同业务之间需要互相交换信息时,把一个应用程序的模型用作通信媒介,那么它可能就无法满足新需求而自由地修改了。

  与现有领域模型进行直接的转换可能不是一种好的解决方案。这些模型可能过于复杂或设计较差。它们可能没有被很好地文档化。如果把其中一个模型作为数据交换语言,它实质上就被固定住了,而无法满足新的开发需求。

  Open Host Service使用一个标准化的协议来支持多方集成。它使用一个领域模型来在各系统间进行交换,尽管这些系统的内部可能并不使用该模型。我们可以更进一步----发布这种语言,或找到一种已经公开发布的语言。

  把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。

XML(可扩展标记语言)使数据交换变得更加容易。XML的一个非常有价值的特性是通过DTD(文档类型定义)或XML模式来正式定义一个专用的领域语言,从而使得数据可以被转换为这种语言。

Core Domain(核心域)

  在设计大型系统时,有非常多的组成部分,它们都很复杂而且对开发的成功也至关重要,但这导致真正的业务资产,领域模型最为精华的部分被掩盖和忽略了。

  一个严峻的现实是我们不可能对所有设计部分进行同等的精化,而是必须分出优先级。为了使领域模型成为有价值的资产,必须整齐地梳理出模型的真正核心,并完全根据这个核心来创建应用程序的功能。但本来就稀缺的高水平开发人员往往会把工作重点放在技术基础设施上,或者只是去解决那些不需要专门领域知识就能理解的领域问题。他们认为通过这些工作可以让自己具备一些在其他地方也能派上用场的专业技能,同时也丰富了个人简历。而真正体现应用程序价值并且使之成为业务资产的领域核心却通常是由那些技术水平稍差的开发人员完成的。如果软件的这部分实现得很差,那么无论技术基础设施有多好,无论支持功能有多完善,应用程序永远都不会为用户提供真正有吸引力的功能。

  在制定项目规划的时候,必须把资源分配给模型和设计中最关键的部分。要想达到这个目的,在规划和开发期间每个人都必须识别和理解这些关键部分。这些部分是应用程序的标志性部分,也是目标应用程序的核心诉求,它们构成了Core Domain。Core Domain是系统中最有价值的部分。

Generic Subdomain(通用子域)

  模型中有些部分除了增加复杂性以外并没有捕捉或传递任何专门的知识。任何外来因素都会是Core Domain愈发的难以分辨和理解。模型中充斥着大量众所周知的一般原则,或者是专门的细节,这些细节并不是我们的主要关注点,而只是起到支持作用。

  识别出那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并放到单独的Module中。任何专有的东西都不应该放在这些模块中。把它们分离出来以后,在继续开发的过程中,它们的优先级应低于Core Domain的优先级,并且不要分派核心开发人员来完成这些任务。此外还可以考虑为这些Generic Subdomain使用现成的解决方案或“公开发布的模型”(Published Model)

各个行业都需要某种形式的企业组织图。
很多应用程序都需要跟踪应收账款、开支分类账和其他财务事项

Domain Vision Statement(愿景陈述)

  在项目开始时,模型通常并不存在,但是模型开发的需求是早就确定下来的重点。在后面的开发阶段,我们需要解释清楚系统的价值,但这并不需要深入地分析模型。此外,领域模型的关键方面可能跨越多个BOUNDED CONTEXT,而且从定义上看,无法将这些彼此不同的模型组织起来表明其共同的关注点。

  很多项目团队都会编写“愿景说明”以便管理。最好的愿景说明会展示出应用程序为组织带来的具体价值。一些愿景说明会把创建领域模型当作一项战略资产。通常,愿景说明文档在项目启动以后就被弃之不用了,而在实际开发过程中从来不会使用它,甚至根本不会有技术人员去阅读它。DOMAIN VISION STATEMENT就是模仿这类文档创建的,但它关注的重点是领域模型的本质,以及如何为企业带来价值。

  写一份CORE DOMAIN的简短描述(大约一页纸)以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方利益的。这份描述要尽量精简。尽早把它写出来,随着新的理解随时修改它。

Highlighted Core(突出核心)

  尽管团队成员可能大体上知道核心领域是由什么构成的,但CORE DOMAIN中到底包含哪些元素,不同的人会有不同的理解,甚至同一个人在不同的时间也会有不同的理解。如果我们总是要不断过滤模型以便识别出关键部分,那么就会分散本应该投入到设计上的精力,而且这还需要广泛的模型知识。因此,CORE DOMAIN必须要很容易被分辨出来。

  1、精炼一个非常简短的文档(3~7页),用于描述Core Domain以及Core元素之间的主要交互过程。
  2、把模型的主要存储库中的Core Domain标记出来,不用特意去阐明其角色。使开发人员很容易就知道什么在核心内,什么在核心外

Cohesive Mechanism(内聚机制)

  封装机制是面向对象设计的一个基本原则。把复杂的算法隐藏到方法中,再为方法起一个一看就知道其用途的名字,这样就把“做什么”和“如何做”分开了。这种技术使设计更易于理解和使用。然而计算有时会非常复杂,使设计开始变得膨胀。机械性的“如何做”大量增加,把概念性的“做什么”完全掩盖了。为解决问题提供算法的大量方法掩盖了那些用于表达问题的方法。

  把概念上的Cohesive Mechanism(内聚机制)分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。用一个Intention-Revealing Interface(柔性设计)来暴露这个框架的功能。这样领域中的其他元素就可以只专注于如何表达问题(做什么)了,而把解决方案的复杂细节(如何做)转移给了框架。

  Generic Subdomain与Cohesive Mechanism的动机是相同的----都是为Core Domain减负。区别在于二者所承担的职责的性质不同。Generic Subdomain是以描述性的模型作为基础的,它用这个模型表示出团队会如何看待领域的某个方面。在这一点上它与Core Domain没什么区别,只是重要性和专门程度较低而已。Cohesive Mechanism并不表示领域,它的目的是解决描述性模型所提出来的一些复杂的计算问题。

Segregated Core(分离核心)

  模型中的元素可能有一部分属于Core Domain,而另一部分起支持作用。核心元素可能与一般元素紧密耦合在一起。Core的概念内聚性可能不是很强,看上去也不明显。这种混乱和耦合性关系抑制了Core。设计人员如果无法清晰地看到最重要的关系,就会开发出脆弱的设计。

  通过把Generic Subdomain提取出来,可以从领域中清除一些干扰性的细节,使Core变得更清楚。但识别和澄清所有这些子领域是很困难的工作,而且有些工作看起来并不值得去做。

  对模型进行重构,把核心概念从支持性元素(包括定义得不清楚的那些元素)中分离出来,并增强Core的内聚性,同时减少它与其他代码的耦合。把所有通用元素或支持性元素提取到其他对象中,并把这些对象放到其他的包中----即使这会把一些紧密耦合的元素分开。

Abstract Core(抽象核心)

  处理大模型的方法通常是把它分解为足够小的子领域,以便能够掌握它们并把它们放到一些独立的Module中。这种简化式的打包风格通常是行之有效的,能够使一个复杂的模型变得易于管理。但有时创建独立的Module反而会使子领域之间的交互变得晦涩难懂,甚至变得更复杂。

  当不同Module的子领域之间有大量交互时,要么需要在Module之间创建很多引用,这在很大程度上抵消了划分模块的价值;要么就必须间接地实现这些交互,而后者会使模型变得晦涩难懂。

  我们不妨考虑采用横向切割而不是纵向切割的方式。多态性(polymorphism)允许我们忽略抽象类型实例的很多细节变化。如果Module之间的大部分交互都可以在多态接口这个层次上表达出来,那么就可以把这些类型重构到一个特定的Core Module中。只有当领域中的基本概念能够用多态接口来表达时,这才是一种有价值的技术。在这种情况下,把这些分散注意力的细节分离出来可以使Module解耦,同时可以精炼出一个更小、更内聚的Core Domain。

  把模型中最基本的概念识别出来,并分离到不同的类、抽象类或接口中。设计这个抽象模型,使之能够表达出重要组件之间的大部分交互。把这个完整的抽象模型放到它自己的Module中,而专用的、详细的实现类则留在由子领域定义的Module中。现在,大部分专用的类都将引用Abstract Core Module,而不是其他专用的Module。Abstract Core提供了主要概念及其交互的简化视图。

相关文章

  • 领域驱动设计(DDD)实现之路

    2004年,当Eric Evans的那本《领域驱动设计——软件核心复杂性应对之道》(后文简称《领域驱动设计》)出版...

  • 领域驱动设计DDD

    最近在换工作,利用间隙看了两本领域驱动设计的经典书籍:《领域驱动设计:软件核心复杂性应对之道》,《实现领域驱动设计...

  • 领域驱动设计释厄录

    基础 学习DDD看的书《实现领域驱动设计》基础只是,概念《领域驱动设计:软件核心复杂性应对之道 》使用,流程,串联...

  • Reading List 2019

    1.Science 2.Technology Architecture 领域驱动设计:软件核心复杂性应对之道[ht...

  • 《领域驱动设计:软件核心复杂性应对之道》Eric Evans 2

    《领域驱动设计:软件核心复杂性应对之道》Eric Evans 2016年版本 序言: 关注领域,关注核心领域,关注...

  • 万字长文,结合电商支付业务一文搞懂DDD

    2004 年,软件大师 Eric Evans 的不朽著作《领域驱动设计:软件核心复杂性应对之道》面世,从书名可以看...

  • DDD & CQRS & Event Sourcing

    一、 DDD分层架构 Evans在它的《领域驱动设计:软件核心复杂性应对之道》书中推荐采用分层架构去实现领域驱动设...

  • 《实现领域驱动设计》笔记(1)-开卷有益总览

    最近一鼓作气买了两本久负盛名的书《领域驱动设计 软件核心复杂性应对之道 》和《实现领域驱动设计》。开卷有益...

  • 微服务架构风格的DDD

    领域驱动设计的源头书籍 关于DDD能找到的最早的一本书是《领域驱动设计 软件核心复杂性应对之道》,2003年Eri...

  • 2022-09-07

    《领域驱动设计:软件核心复杂性应对之道》,Eric Evans 著,陈大峰 等 译,英文原版出版于2003年 参考...

网友评论

      本文标题:领域驱动设计 —— 软件核心复杂性应对之道

      本文链接:https://www.haomeiwen.com/subject/apybbdtx.html