华企号 后端开发 什么是模型设计?

什么是模型设计?

为了保证软件实践得简洁并且与模型保持一致,不管实际情况如何复杂,必须运用建模和设计的实践。

某些设计决策能够使模型和程序紧密结合在一起,互相促进对方的效用。这种结合要求我们注意每个元素的细节,对细节问题的精雕细琢能够打造一个稳定的平台。

本部分主要将一些模式,说明细微的模型差别和设计决策如何影响领域驱动设计的过程。

下面的简图是一张导航图,它描述的是本部分所要讲解的模式以及这些模式彼此关联的方式。

共用这些标准模式可以使设计有序进行,也使项目组成员能够更方便地了解彼此的工作内容。同时,使用标准模式也使 Ubiquitous Language 更加丰富,所有的项目组成员都可以使用 Ubiquitous Language  来讨论模型和设计决策。

开发一个好的领域模型是一门艺术。而模型中各个元素的实际设计和实现则相对系统化。将领域设计和软件系统中的其他关注点分离会使设计与模型之间的关系非常清晰。根据不同的特征来定义模型元素则会使元素的意义更加鲜明。对每个元素使用已验证的模式有助于创建更易于实现的模型。

 

1. 分离领域

在软件中,虽然专门用于解决领域问题的那部分通常只占整个软件系统的很小一部分,但其却出乎意料的重要。要想实现 Model- Driver-Design(模型驱动设计)的想法,我们需要着眼于模型中的元素并且将它们视为一个系统。绝不能被迫从一大堆混杂的对象中将领域对象挑选出来。

我们需要将领域对象与系统中的其他功能分离,这样就能够避免将领域概念和其他只与软件技术相关的概念搞混了,也不会在纷繁复杂的系统中完全迷失了领域。

 

  1.1 模式:Layered Architecture(分层架构)

什么是模型设计?插图

 

在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而一些业务逻辑则会被嵌入到用户界面组件和数据库脚本中。这么做就是为了以最简单的方式在短期内完成开发工作。

如果与领域有关的代码分散在大量的其他代码之中,那么查看和分析领域代码就会变得异常困难。对用户界面的简单修改实际上很可能会改变业务逻辑,而想要调整业务规则也很可能需要对用户界面代码、数据库操作代码或者其他的程序元素进行仔细的筛查。这样就不太可能实现一致的、模型驱动的对象了,同时也会给自动化测试带来困难。考虑到程序中各个活动所涉及的大量逻辑和技术,程序本身必须简单明了,否则就会让人无法理解。

 

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

软件系统有各种各样的划分方式,但是根据软件行业的经验和惯例,普遍采用 Layered Architecture(分层架构),特别是有几个层已成了标准层。Layered Architecture 的基本原则是层中任何元素都依赖于本层的其他元素或其下层的元素。向上的通信必须通过间接的方式进行,这些将在后面讨论。

分层的价值在于每一层都只代表程序中的某一特定方面。这种限制使每个方面的设计都更具内聚性,更容易理解。当然,要分离出内聚设计中最重要的方面,选择恰当的分层方式是至关重要的。大多数架构使用的都是下面这4个概念层的某种变体。

什么是模型设计?插图1

 

有些项目没有明显划分出用户界面层和应用层,而有些项目则有很多个基础设施层。但是将领域层分离出来才是实现 Model- Driver-Design 的关键。

因此:

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

将领域层与基础设施层以及用户界面层分离,可以使每层的设计更加清晰。彼此独立的层更容易维护,因为它们往往以不同的速度发展并且满足不同的需求。层与层的分离也有助于在分布式系统中部署程序,不同的层可以灵活地放在不同服务器或者客户端中,这样可以减少通信开销,并优化程序性能。

 

  1.1.1 将各层关联起来

各层之间需要互相连接,在连接各层的同时不影响分离带来的好处,这是很多模式的目的所在。

各层之间是松散连接的,层与层的依赖关系只能是单向的。上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的),以及采用常规的交互手段。而如果下层元素需要与上层元素进行通信(不只是回应直接查询),则需要采用另一种通信机制,使用架构模式来连接上下层,如回调模式或者 Observes模式(观察者模式)。

还有许多其他连接用户界面和应用层的方式。对我们而言,只要连接方式能够维持领域层的独立性,保证在设计领域对象时不需要同时考虑可能与其交互的用户界面,那么这些连接方式都是可用的。

通常,基础设施层不会发起领域层中的操作,它处于领域层之下,不包含其所服务的领域中的知识。事实上这种技术能力最常以 Service 的形式提供。

应用层和领域层可以调用基础设施层所提供的 Service。然而,并不是所有的基础设施都以可供上层调用的 Service 的形式出现的。有些技术组件被设计成直接支持其他层的基本功能(如为所有的领域对象提供抽象基类),并且提供关联机制(如 MVC 及类似框架的实现)。这种 “架构框架” 对于程序其他部分的设计有着更大的影响。

 

  1.1.2 架构框架

如果基础设施通过接口调用 Service 的形式来实现,那么如何分层以及如何保持层与层之间的松散连接就相当显而易见了。但是有些技术问题要求更具有侵入性的基础设施。整合了大量基础设施需求的框架通常会要求其他层以某种特定的方式实现,如以框架类的子类形式或者带有结构化的方法签名。(子类在父类的上层似乎是违反常理的,但是要记住哪个类反映了另一个类的更多知识。)最好的架构框架既能解决复杂技术问题,也能让领域开发人员集中精力去表达模型,而不考虑其他问题。然而使用框架很容易为项目制造障碍:要么设定了太多的假设,减小了领域设计的可选范围;要么是需要实现太多的东西,影响开发进度。

项目中一般都需要某种形式的架构框架(尽管有时候项目团队选择了不太合适的框架)。当使用框架时,项目团队应该明确其使用目的:建立一种可以表达领域模型的实现并且用它来解决重要问题。项目团队必须想方设法让框架满足这些需求,即使这意味着抛弃框架中的一些功能。

不妄求万全之策,只要有选择性地运用框架来解决难点问题,就可以避开框架的很多不足之处。明智而谨慎地选择框架中最具有价值的功能能够减少程序实现和框架之间的耦合,使随后的设计决策更加灵活。更重要的是,现在许多框架的用法都极其复杂,这种简化方式有助于保持业务对象的可读性,使其更富有表达力。

 

  1.2 领域层是模型的精髓

现在,大部分软件系统都采用了 Layered Architecture ,只是采用的分层方案存在不同而已。许多类型的开发工作都能从分层中受益。然而,领域驱动设计只需要一个特定的层存在即可。

领域模型是一系列概念的集合。“领域层” 则是领域模型以及所有与其直接相关的设计元素的表现,它由业务逻辑的设计和实现组成。在 Model- Driven Design 中,领域层的软件构造反应出了模型概念。

如果领域逻辑与程序中的其他关注点混在一起,就不可能实现这种一致性。将领域实现独立出来是领域驱动设计的前提。

 

  1.3 模式:The Smart UI “反模式”

如果一个经验并不丰富的项目团队要完成一个简单的项目,却决定使用 Model-Driven Design 以及 Layered Architecture ,那么这个项目组将经历一个艰难的学习过程。团队成员不得不去掌握复杂的技术,艰难地学习对象建模。对基础设施和各层的管理工作使得原本简单的任务却要花费很长时间来完成。简单项目的开发周期短,期望值也不是很高。所以,早在项目团队完成任务之前,该项目就会被取消,更谈不上去论证有关这种方法的许多种令人激动的可行性了。

即使项目有更充裕的时间,如果没有专家的帮助,团队成员也不太可能掌握这些技术。最后,加入他们确实能够克服这些困难,恐怕也只会开发出一套简单的系统。因为这个项目本来就不需要丰富的功能。

 

因此,当情况需要时:

在用户界面中实现所有的业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储库。使用自动化程度最高的用户界面创建工具和可用的可视化编程工具。

但,在领域驱动设计中,将 The Smart UI 看作是 “反模式” 。然而在其他情况下,它也是完全可行的。

如果项目团队有意识地应用这个模式,那么就可以避免其他方法所需要的大量开销。项目团队常犯的错误是采用一种复杂的设计方法,却无法保证项目从头到尾始终使用它。另一种常见的也是代价高昂的错误则是为项目构建一种复杂的基础设施以及使用工业级的工具,而这样的项目根本不需要它们。

 

这里讨论 Smart UI 只是为了让我们认清为什么以及何时需要采用诸如 Layered Architecture 这样的模式来分离出领域层。

如果一个架构能够把那些与领域相关的代码隔离出来,得到一个内聚的领域设计,同时又使领域设计与系统其他部分保持松散耦合,那么这种架构也许可以支持领域驱动设计。

 

  1.4 其他分离方式

除了基础设施和用户界面之外,还有一些其他的元素也会破坏精心设计的领域模型。必须要考虑那些没有完全集成到模型中的领域元素。不得不与同一领域中使用不同模型的其他开发团队合作。还有其他的因素会让你的模型结构不再清晰,并且影响模型的使用效率。后面会讨论这方面的问题,同时会介绍其他模式。非常复杂的领域模型本身是难以使用的,后面会说明如何在领域层内进行进一步分区,以便从次要细节中凸显出领域的核心概念。

接下来讨论一些具体细节,即如何让一个有效的领域模型和一个富有表达力的实现同时演进。毕竟,把领域隔离出来的最大好处就是可以真正专注于领域设计,而不用考虑其他方面。

 

2. 软件中所表示的模型

要想在不削弱模型驱动设计能力的前提下对实现做出一些这种,需要重新组织基本元素并理解它们。我们需要将模型与实现的各个细节一一联系起来。以下主要讨论这些基本模型元素。

下文的讨论从如何设计和简化关联开始。对象之间的关联很容易想出来,也很容易画出来,但实现它们却存在很多潜在的麻烦。关联也表明了具体的实现决策在 Model- Driven- Design 中的重要性。

本文的讨论将侧重于模型本身,但仍继续考察具体模型选择与实现问题之间的关系,将着重区分用于用于表示模型的3种模型元素模式: Entity、Value Object 和 Service。

从表面上看,定义那些用来捕获领域概念的对象很容易,单要想反映其含义却很困难。这要求我们明确区分各种模型元素的含义,并与一系列设计实践结合起来,从而开发出特定类型的对象。

一个对象是用来表示某种具有连续性和标识的事物的呢(可以跟踪它所经历的不同状态,甚至可以跨不同的实现跟踪它),还是用于描述某种状态的属性呢?这是 Entity 和 Value Object 之间的根本区别。明确地选择这两种模式中的一个来定义对象,有利于减少分歧,并帮助我们做出特定的选择,这样才能得到健壮的设计。

 

领域中还有一些方面适合用动作或操作来表示,这比用对象表示更加清楚。这些方面最好用 Service 来表示,而不应把操作的责任强加到 Entity 或 Value Object 上,尽管这样做稍微违背了面向对象的建模传统。Service 是应客户端请求来完成某事。在软件的技术层中有很多Service。在领域中也可以使用 Service ,当对软件要做的某项无状态的活动进行建模时,就可以将该活动作为一项 Service

最后,Module 的讨论将有助于理解这样一个要点——每个设计决策都应该是在深入理解领域中的某些深层知识之后做出的。高内聚、低耦合这种思想(通常被认为是一种技术指标)可应用于概念本身。在 Model- Driven Design 中,Module 是模型的一部分,它们应该反映领域中的概念。

 

  2.1 关联

对象之间的关联使得建模与实现之间的交互更为复杂。

模型中每个可遍历的关联,软件中都要有相同属性的机制。

 

一个显示了顾客与销售代表之间关联的模型有两个含义。一方面,它把开发人员所认为的两个真实的人之间的关系抽象出来。另一方面,它相当于两个 Java 对象之间的对象指针,或者相当于数据库查询(或类似实现)的一种封装。

例如,一对多关联可以用一个集合类型的实例变量来实现。但设计无需如此直接。可能没有集合,这时可以使用一个访问方法(accessor method)来查询数据库,找到相应的记录,并用这些记录来实例化对象。这两种设计方法反映了同一个模型。设计必须制定一种具体的遍历机制,这种遍历的行为应该与模型中的关联一致。

 

现实生活中有大量 “多对多” 关联,其中有很多关联天生就是双向的。我们在模型开发的早期进行头脑风暴活动并探索领域时,也会得到很多这样的关联。但这些普遍的关联会使实现和维护变得很复杂。此外,它们也很少能表示出关系的本质。

至少有三种方法可以使得关联更易于控制。

(1)规定一个遍历方向。

(2)添加一个限定符,以便有效地减少多重关联。

(3)消除不必要的关联。

尽可能地对关系进行约束是非常重要的。双向关联意味着只有将这两个对象放在一起考虑才能理解它们。当应用程序不要求双向遍历时,可以指定一个遍历方向,以便减少互相依赖,并简化设计。理解了领域之后就可以自然地确定一个方向。

 

限定多对多关联的遍历方向可以有效地将其实现简化为一对多关联,从而得到一个简单的多的设计。

坚持将关联限定为领域所倾向的方向,不仅可以提高这些关联的表达力并简化其实现,而且还可以突出剩下的双向关联的重要性。当双向关联是领域的一个语义特征时,或者当应用程序的功能要求双向关联时,就需要保留它,以便表达出这些需求。

当然,最终的简化是清除那些对当前工作或模型对象的基本含义来说不重要的关联。

 

从仔细地简化和约束模型的关联到 Model-Driven Design ,还有一段漫长的探索过程。现在我们转向面向对象本身。仔细区分对象可以使得模型更加清晰,并得到更实用的实现。

 

  2.2 模式:Entity(又称为 Reference Object)

很多对象不是通过它们的属性定义的,而是通过连续性和标识定义的。我们一般认为,一个人有一个标识,这个标识会陪伴他走完一生()甚至死后。这个人的物理属性会发生变化,最后消失。他的名字可能改变,财务关系也会发生变化,没有哪个属性是一生不变的,但标识却是永久的。我跟我5岁时是同一个人吗?稍微变化一下问题的角度:应用程序的用户是否关心现在的5和5岁时的我是不是同一个人?

 

在对象的多个市县、存储形式和真实世界的参与者之间,概念性标识必须是匹配的。属性可以不匹配,例如,销售代表可能已经在联系软件中更新了地址,而这个更新正在传送给到期应收账款软件。两个客户可能同名。在分布式软件中,多个用户可能从不同地点输入数据,这需要在不同的数据库中异步地协调这些更新事物,使它们传播到整个系统。

 

对象建模有可能把我们的注意力引到对象的属性上,但实体的基本概念是一种贯穿整个生命周期的抽象的连续性。

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

主要由标识定义的对象被称作 Entity 。Entity(实体)有特殊的建模和设计思路。它们具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了有效地跟踪这些对象,必须定义它们的标识。它们的类定义、职责、属性和关联必须由其标识来决定,而不是依赖其所具有的属性。即使对于那些不发生根本变化或者生命周期不太复杂的 Entity ,也应该在语义上把它们作为 Entity 来对待,这样可以得到更清晰的模型和更健壮的实现。

当然,软件系统中的大多数 “Entity” 并不是人,也不是其通常意义上所指的 “实体” 或者 “存在”。Entity 可以是任何事物,只要满足两个条件即可。一是它在生命周期中具有连续性,二是它的区别并不是由那些对用户非常重要的属性决定的。Entity 可以是一个人、一座城市、一辆汽车或一次交易。

另一方面,在一个模型中,并不是所有对象都是具有有意义标识的Entity。但是,由于面向对象语言存在每个对象都构建了一些与 “标识” 有关的操作(如Java中的 “==” 操作符),这些操作通过比较两个引用在内存中的位置(或通过其他机制)来确定这两个引用是否指向同一个对象。但这种标识机制在其他应用领域中却没什么意义。标识是Entity的一个微妙的、有意义的属性,我们是不能把它交给语言的自动特性来处理的。

标识的重要性并不仅仅体现在特定的软件系统中,在软件系统之外它通常也是非常重要的,如银行交易。但有时标识只有在系统上下文中才重要,如一个计算机进程的标识。

因此:

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

在现实世界中,并不是每个事物都必须有一个标识,标识重不重要,完全取决于它是否有用。实际上,现实世界中的同一个事物在领域模型中可能需要表示为 Entity ,也可能不需要表示为 Entity。

 

  2.2.1 Entity 建模

当对一个对象进行建模时,我们自然而然会考虑它的属性,而且考虑它的行为也显得非常重要。但 Entity 最基本的职责是确保连续性,以便使其行为更清楚且可预测。保持实体的简练是实现这一责任的关键。不要将注意力集中在属性或行为上,应该摆脱这些细枝末节,抓住 Entity 对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。此外,应该将行为和和属性转移到与核心实体关联的其他对象中。这些对象中,有些可能是 Entity,有些可能是 Value Object。除了标识问题之外,实体往往通过协调其关联对象的操作来完成自己的职责。

 

  2.2.2 设计标识操作

每个 Entity 都必须有一种建立标识的操作方式,以便与其他对象区分开,即使这些对象与它具有相同的描述属性。不管系统是如何定义的,都必须确保标识属性在系统中是唯一的,即使是在分布式系统中,或者对象已被归档,也必须确保标识的唯一性。

有时,某些数据属性或属性组合可以它们在系统中具有唯一性,或者在这些属性上加一些简单约束可以使其具有唯一性。这种方法为 Entity 提供了唯一键。

当对象属性没办法形成真正唯一键时,另一种经常用到的解决方案是为每个实例附加一个在类中唯一的符号(如一个数字或字符串)。一旦这个ID符号被创建并存储为 Entity 的一个属性,必须将它指定为不可变的。它必须永远不变,即使开发系统无法直接强制这条规则。例如,当对象被扁平化到数据库中或从数据库中重新创建时,ID 属性应该保持不变。有时可以利用技术框架来实现此目的,但如果没有这样的框架,就需要通过工程纪律来约束。

当自动生成ID时,用户可能永远不需要看到它。ID可能只是在内部需要,例如,在一个可以按人名查找记录的联系人管理应用程序中。这个程序需要用一种简单、明确的方式来区分两个同名联系人,这就可以可以通过唯一的内部ID来实现。在检索出两个不同的条目之后,系统将显示这两个不同的联系人,但可能不会显示ID。用户可以通过这两个人的公司、地点等属性来区分他们。

在有些情况下用户会对生成的ID感兴趣。当我委托一个包裹运送服务寄包裹时,我会得到一个跟踪号,它是由运送公司的软件生成的,我可以用这个号码来识别和跟踪我的包裹。当我预订机票或酒店时,会得到一个确认号码,它是预订交易的唯一标识符。

在某些情况下,需要确保ID在多个计算机系统之间具有唯一性。例如,如果需要在两家具有不同计算机系统的医院之间交换医疗记录,那么理想情况下对同一病人应该使用同一个ID,但如果这两个系统各自生成自己的ID,这就很难实现。这样的系统通常使用由另外一家机构发放的标识符。

 

  2.3 模式:Value Object

很多对象没有概念上的标识,它们描述了一个事务的某种特征。

跟踪 Entity 的标识是非常重要的,但为其他对象也加上标识会影响系统性能并增加分析工作,而且会使模型变得混乱,因为所有对象看起来都是相同的。

  软件设计要时刻与复杂性做斗争。我们必须区别对待问题,仅在真正需要的地方进行特殊处理。

  然而,如果仅仅把这类对象当作没有标识的对象,那么就忽略了它们的工具价值或术语价值。事实上,这些对象有其自己的特征,对模型也有着自己的重要意义。这些是用来描述事物的对象。

用于描述领域的某个方面而本身没有概念标识的对象称为 Value Object(值对象)。Value Object 被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心他们是谁。

 

Value Object 甚至可以引用 Entity。例如,如果我请在线地图服务为我提供一个从旧金山到洛杉矶的驾车风景路线,它可能会得出一个 “路线” 对象,此对象通过太平洋海岸公路连接旧金山和洛杉矶。这个 “路线” 对象是一个Value,尽管它所引用的三个对象(两座城市和一条公路)都是 Entity。

Value Object 经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。Value Object 可以用作 Entity(以及其他 Value)的属性。我们可以把一个人建模为一个具有标识的 Entity,但这个人的名字是一个 Value。

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

Value Object 所包含的属性应该形成一个概念整体。例如,street、city不应是Person 对象的单独的属性。它们是整个地址的一部分,这样可以使得 Person 对象更简单,并使地址成为一个人更一致的 Value Object。

 

  2.3.1 设计 Value Object

我们并不关心使用的是 Value Object 的哪个实例。由于不受这方面的约束,设计可以获得更大的自由,因此可以简化设计或优化性能。在设计 Value Object 时有多种选择,包括复制、共享或保持 Value Object 不变。

 

两个人同名并不意味着他们是同一个人,也不意味着他们是可以互换的。但表示名字的对象是可以互换的,因为他们只涉及名字的拼写。一个 Name 对象可以从第一个 Person 对象复制给第二个 Person 对象。

事实上,这两个Person对象可能不需要自己的名字实例,它们可以共享同一个 Name 对象(其中每个 Person 对象都有一个指向同一个名字实例的指针),而无需改变它们的行为或标识。如此一来,当修改其中一个人名字时就会产生问题,这时另一个人的名字也将改变!为了防止这种错误发生,以便安全地共享一个对象,必须确保 Name 对象是不变的——它不能改变,除非将其整个替换掉。或者传递一个副本来解决。

 

Value Object 为性能优化提供了更多选择,这一点可能很重要,因为 Value Object 往往为数众多。在大型系统中,这种效果可能会被放大数千倍,而且这样的优化可能决定一个系统是可用的,还是由于数百万个多余对象而变得异常缓慢。这只是无法应用于 Entity 的优化技巧中的一个。

复制和共享哪个更划算取决于实现环境。虽然复制有可能导致系统被大量的对象阻塞,但共享可能会减慢分布式系统的速度。当两个机器之间传递一个副本时,只需发送一条消息,而且副本达到接收端后是独立存在的。但如果共享一个实例,那么只会传递一个引用,这要求每次交互都要向发送方返回一条消息。

以下几种情况最好使用共享,这样可以发挥共享的最大价值并最大限度地减少麻烦:

节省数据库空间或减少对象数量是一个关键要求时;

通信开销很低时(如在中央服务器中);

共享的对象被严格限定为不可变时。

 

在有些语言和环境中,可以将属性或对象声明为不可变的,但有些却不具备这种能力。这种声明能够体现出设计决策,但它们并不是十分重要。我们在模型中所做的很多区别都无法用当前工具和编程语言在实现中显式地声明出来。例如,我们无法声明 Entity 并自动确保其具有一个标识操作。但是,编程语言没有直接支持这些概念的区别并不说明这些区别没有用处。这只是说明我们需要更多的约束机制来确保一些重要的规则(这些规则只有在实现中才是隐式的)。命名规则、精心准备的文档和大量讨论都可以强化这些需求。

只要Value Object 是不可变的,变更管理就会很简单,因为除了整体替换之外没有其他的更改。不变的对象可以自由共享。如果垃圾回收时可靠的,那么删除操作就只是将所有指向对象的引用删除。当在设计中将一个Value Object指定为不可变时,开发人员就可以完全根据技术需求来决定是使用复制,还是使用共享,因为他们没有后顾之忧——应用程序不依赖于对象的特殊实例。

 

在有些情况下出于性能考虑,仍需要让Value Object 是可变的。这包括以下因素:

如果 Value 频繁改变;

如果创建或删除对象的开销很大;

如果替换(而不是修改)将打乱集群;

如果 Value 的共享不多,或者共享不会提高集群性能,或其他某种技术原因。

 

  2.3.2 设计包含 Value Object 的关联

前面说的与关联有关的大部分内容也适用于 Entity 和 Value Object 。模型中的关联越少越好,越简单越好。

但是,两个 Value Object 之间的双向关联则完全没有意义。当一个 Value Object 指向另一个 Value Object 时,由于没有标识,说一个对象指向的对象正是那个指向它的对象并没有任何意义的。我们充其量只能说,一个对象指向的对象与那个指向它的对象是等同的,但这可能要求我们必须在某个地方实施这个固定规则。而且,尽管我们可以这样做,并设置双向指针,但很难想出这种安排有什么用处。因为,我们应尽量完全清楚Value Object 之间的双向关联。如果在你的模型中看起来确实需要这种关联,那么首先应重新考虑一下将对象声明为 Value Object 这个决定是否正确。或许它拥有一个标识,而你还没有注意到它。

 

  2.4 模式:Service

有时,对象不是事物。

在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是 Service(服务)。

 

有些重要的领域操作无法放到 Entity 或 Value Object 中。这当中有些操作从本质上讲是一些活动或动作,而不是事物,但由于我们的建模范式是对象,因此要想办法讲它们划归到对象这个范畴里。

现在,一个比较常见的错误是没有努力为这类行为找到一个适当的对象,而是逐渐转为过程化的编程。但是,当我们勉强将一个操作放到不符合对象定义的对象中时,这个对象就会产生概念上的混淆,而且会变得很难理解或重构。复杂的操作很容易把一个简单对象搞乱,使对象的角色变得模糊。此外,由于这些操作常常会牵扯到很多领域对象——需要协调这些对象以便使它们工作,而这会产生对所有这些对象的依赖,将那些本来可以单独理解的概念掺杂在一起。

有时,一些 Service 看上去就像是模型对象,他们以对象的形式出现,但除了执行一些操作之外并没有其他意义。这些 “实干家” 的名字通常以 “Manager” 之类的名字结尾。它们没有自己的状态,而且除了所承载的操作之外在领域中也没有其他意义。尽管如此,该方法至少为这些特立独行的行为找到一个容身之所,避免它们扰乱真正的模型对象。

 

  一些领域概念不适合被建模为对象。如果勉强把这些重要的领域功能归为 Entity 或 Value Object 的职责,那么不是歪曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。

Service 是作为接口提供的一种操作,它在模型中是独立的,它不像 Entity 和 Value Object 那样具有封装的状态。Service 是技术框架的一种常见模式,但它们也可以在领域层中使用。

所谓 Service,它强调的是与其他对象的关系。与 Entity 和 Value Object 不同,它只是定义了能够为客户做什么。Service 往往是以一个活动来命名,而不是以一个 Entity 来命名,也就是说,它是动词而不是名词。Service 可以有抽象而有意义的定义,只是它使用了一种与对象不同的定义风格。Service 也应该有定义的职责,而且这种职责以及履行它的接口也应该作为领域模型的一部分来加以定义。操作名称应来自于 Ubiquitous Language ,如果 Ubiquitous Language 中没有这个名称,则应该将其引入到 Ubiquitous Language 中。参数和结果应该是领域对象。

 

使用 Service 时应谨慎,它们不应该替代 Entity 和 Value Object 的所有行为。但是,当一个操作实际上是一个重要的领域概念时,Service 很自然就会成为 Model- Driven Design 中的一部分。将模型中的独立操作声明为一个 Service,而不是声明为一个不代表任何事情的虚拟对象,可以避免对任何人产生误导。

好的 Service 有以下3个特征:

(1)与领域概念相关的操作不是 Entity 或 Value Object 的一个自然组成部分。

(2)接口是根据领域模型的其他元素定义的。

(3)操作是无状态的。

这里所说的无状态是指任何客户都可以使用某个 Service 的任何实例,而不必关心该实例的历史状态。Service 执行时将使用可全局访问的信息,甚至会更改这些全局信息(也就是说,它可能具有副作用)。但 Service 不保持影响其自身行为的状态,这一点与大多数领域对象不同。

  当领域中的某个重要的过程或转换操作不是 Entity 或 Value Object 的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为 Service 。定义接口时要使用模型语言并确保操作名称是 Ubiquitous Language 中的术语。此外,应该使 Service 成为无状态的。

 

  2.4.1 Service 与孤立的领域层

这种模式只重视那些在领域中具有重要意义的 Service,但 Service 并不只是在领域层中使用。我们需要注意区分属于领域层的 Service 和那些属于其他层的 Service ,并划分责任,以便将它们明确地区分开。

文献中所讨论的大多数 Service 是纯技术的 Service,它们都属于基础设施层。领域层和应用层的 Service 与这些基础设施层 Service 进行协作。例如,银行可能有一个用于向客户发送电子邮件的应用程序,当客户的账户余额小于一个特定的临界值时,这个程序就向客户发送一封电子邮件。封装了电子邮件系统的接口(也可能是其他的通知方式)就是基础设施层中的 Service。

应用层 Service 和领域层 Service 可能很难区分。应用层负责通知的设置,而领域层负责确定是否满足临界值,尽管这项任务可能并不需要使用 Service,因为它可以作为 “account”(账户)对象的职责中。这个银行应用程序可能还负责资金转账。如果设计一个 Service 来处理资金转账相应的借方和贷方,那么这项功能将属于领域层。资金转账在银行领域语言中是一项有意义的操作,而且它涉及基本的业务逻辑。而纯技术的 Service 应该没有任何业务意义。

 

很多领域或应用层 Service 是在 Entity 和 Value Object 的基础上建立起来的,它们的行为类似于将领域的一些潜在功能组织起来以执行某种任务的脚本。Entity 和 Value Object 往往由于粒度过细而无法提供对领域层功能的便捷访问。我们在这里会遇到领域层和应用层之间很微妙的分界线。例如,如果银行应用程序可以把我们的交易进行转换并到处到一个电子表格文件中,以便进行分析,那么这个导出操作就是应用层 Service 。“文件格式” 在银行领域中是没有意义的,它也不涉及业务规则。

另一方面,账户之间的转账功能属于领域层 Service ,因为它包含重要的业务规则(如处理相应的借方账户和贷方账户),而且“资金转账”是一个有意义的银行术语。在这种情况下,Service 自己并不会做太多的事情,而只是要求两个 Account 对象完成大部分工作。但如果将 “转账” 操作强加在 Account 对象上会很别扭,因为这个操作涉及两个账户和一些全局规则。

我们可能喜欢创建一个 Funds Transfer(资金转账)对象来表示两个账户,外加一些与转账有关的规则和历史记录。但在银行间的网络中进行转账时,仍然需要使用 Service 。此外,在大多数开发系统中,在一个领域对象和外部资源之间建立一个接口是很别扭的。我们可以利用一个 Facade(外观)将这样的外部 Service 包装起来,这个外观可能以模型作为输入,并返回一个 “Funds Transfer” 对象(作为它的结果)。但无论中间涉及什么Service,甚至那些超出我们掌控范围的 Service ,这些 Service 都是在履行资金转账的领域职责。

什么是模型设计?插图2

 

 

 2.4.2 粒度

上述对 Service 的讨论强调的是将一个概念建模为 Service 的表现力,但 Service 还有其他有用的功能,它可以控制领域层中的接口的粒度,并且避免客户端与 Entity 和 Value Object 的耦合。

在大型系统中,中等粒度的、无状态的 Service 更容易被复用,因为它们在简单的接口背后封装了重要功能。此外,细粒度的对象可能导致分布式系统的消息传递的效率低下。

如前所述,由于应用层负责对领域对象的行为进行协调,因此细粒度的领域对象可能会把领域层的知识泄漏到应用层中,这产生的结果是应用层不得不处理复杂的、细致的交互,从而使得领域知识蔓延到应用层或用户界面代码当中,而领域层会丢失这些知识。明智地引入领域层服务有助于在应用层和领域层之间保持一条明确的界限。

这种模式有利于保持接口的简单性,便于客户端控制并提供了多样化的功能。它提供了一种在大型或分布式系统中便于对组建进行打包的中等粒度的功能。而且,有时 Service 是表示领域概念的最自然的方式。

 

  2.4.3 对 Service 的访问

像 J2EE 和 CORBA 这样的分布式系统架构提供了特殊的 Service 发布机制,这些发布机制具有一些使用上的惯例,并且增加了发布和访问功能。但是,并非所有项目都会使用这样的框架,即使在使用了它们的时候,如果只是为了在逻辑上实现关注点的分离,那么它们也是大材小用了。

与分离特定职责的设计决策相比,提供对 Service 的访问机制的意义并不是十分重大。一个 “操作” 对象可能足以作为 Service 接口的实现。我们很容易编写一个简单的 Singleton 对象来实现对 Service 的访问。从编码惯例可以明显看出,这些对象只是 Service 接口的提供机制,而不是有意义的领域对象。只有当真正需要实现分布式系统或充分利用框架功能的情况下才应该使用复杂的架构。

 

  2.5 模式:Module(也称为 Package)

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

Module 从更大角度描述了领域。

 

  每个人都会使用 Module,但却很少有人把它们当作模型中的一个成熟的组成部分。代码按照各种各样的类型进行分解,有时按照技术架构来分割,有时是按照开发人员的任务分工来分割的。甚至那些从事大量重构工作的开发人员也倾向使用项目早起形成的一些 Module 。

  众所周知,Module 之间应该是低耦合的,而在 Module 内部则是高内聚的。耦合和内聚的解释使得 Module 听上去像是一种技术指标,仿佛是根据关联和交互的分布情况来机械地判断它们。然而,Module 并不仅仅是代码的划分,而且也是概念的划分。一个人一次考虑的事情是有限的(因此才要低耦合)。不连贯的思路和 “一锅粥” 似的思想同样难于理解(因此才要高内聚)。

低耦合高内聚作为通用的设计原则既适用于各种对象,也适用于 Module,但 Module 作为一种更粗粒度的建模和设计元素,采用低耦合高内聚原则显得更为重要。

在一个好的模型中,元素之间是要协同工作的,而仔细选择的Module可以将那些具有紧密概念关系的模型元素集中到一起。将这些具有相关职责的对象聚合到一起,可以把建模和设计工作集中到单一 Module 中,这会极大地降低建模和设计的复杂性,使人们可以从容面对这些工作。

 

像领域驱动设计中的其他元素一样,Module 是一种表达机制。Module 的选择应该取决于被划分到模块中的对象的意义。当你将一些类放到 Module 中时,相当于告诉下一位看到你的设计的开发人员要把这些类放在一起考虑。如果说模型讲述了一个故事,那么Module 就是这个故事的各个章节。模块的名称表达了其意义。这些名称应该被添加到 Ubiquitous Language 中。你可能会向一位业务专家说 “现在让我们讨论一下‘客户’模块”,这就为你们接下来的对话设定了上下文。

因此:

选择能够描述系统的 Module ,并使之包含一个内聚的概念集合。这通常会实现 Module 之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找到一个可作为 Module 基础的概念(这个概念先前可能被忽视了),基于这个概念组织的 Module 可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立地理解和分析这些概念。对模型进行精化,直到可以根据高层领域概念对模型进行划分,同时相应的代码不会产生耦合。

  Module 的名称应该是Ubiquitous Languag 中的术语。Module 及其名称应反映出领域的深层只是。

仅仅研究概念关系是不够的,它并不能代替技术措施。这二者是相同问题的不同层次,都是必须完成的。但是,只有以模型为中心进行思考,才能得到更深层次的解决方案,而不是随便找一个解决方案应付了事。当必须做出一个折中选择时,务必保证概念清晰,即使这意味着 Module 之间会产生更多引用,或者更改 Module 偶尔会产生 “涟漪效应” 。开发人员只要理解模型所描述的内容,就可以应付这些问题。

 

技术框架对打包决策有极大的影响,有些技术框架是有帮助的,有些则要坚决抵制。

一个非常有用的框架标准是 Layered Architecture ,它将基础设施和用户界面代码放到两组不同的包中,并且从物理上把领域层隔离到它自己的一组包中。

但从另一个方面看,分层架构可能导致模型对象实现的分裂。一些框架的分层方法是把一个领域对象的职责分散到多个对象当中,然后把这些对象放到不同的包中。这样做除了导致每个组件的实现变得更加复杂以外,还破坏了对象模型的内聚性。对象的一个基本概念是将数据和操作这些数据的逻辑封装在一起。

除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码放在同一模块中(如果不能放在同一对象中的话)。

 

**************************************************

领域模型中的每个概念都应该在实现元素中反映出来。Entity、Value Object、它们之间的关联、领域 Service 以及用于组织元素的 Module 都是实现与模型直接对应的地方。实现中的对象、指针和检索机制必须直接、清楚地映射到模型元素。如果没有做到这一点,就要重写代码,或者回头修改模型,或者同时修改代码和模型。

不要在领域对象中添加任何与领域对象所表示的概念没有紧密关系的元素。领域对象的职责是表示模型。当然,其他一些与领域相关的职责也是要实现的,而且为了使系统工作,也必须管理其他数据,但它们不属于领域对象。后面讲讲到一些支持对象,这些对象履行领域层的技术职责,如定义数据库搜索和封装复杂的对象创建。

上述介绍的4种模式为对象模型提供了构造块。但 Model- Driven Design 并不是说必须将每个元素都建模为对象。一些工具还支持其他的模型范式,如规则引擎。项目需要在它们之间做出契合实际的折中选择。这些其他的工具和技术是 Model- Driven Design 的补充,而不是要取而代之。

 

  2.6 建模范式

Model- Driven Design 要求使用一种与建模范式协调的实现技术。目前主流的范式是面向对象设计,这种范式的流行有许多原因,包括对象本身的固有因素和一些环境因素。

不管在项目中使用哪种主要的模型范式,领域中都会有一些部分更容易用其他范式来表示。当领域中只有个别元素适合用其他范式时,开发人员可以接受一些蹩脚的对象,以使整个模型保持一致。

作者: 华企网通李铁牛程序员

我是程序员李铁牛,热爱互联网软件开发和设计,专注于大数据、数据分析、数据库、php、java、python、scala、k8s、docker等知识总结。15889726201 我的座右铭:"业精于勤荒于嬉,行成于思毁于随"
上一篇
下一篇

发表回复

联系我们

联系我们

028-84868647

在线咨询: QQ交谈

邮箱: tech@68v8.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部