领域驱动设计-软件核心复杂性应对之道:第五章

第五章 软件中所表示的模型

着重区分用于表示模型元素的三种模式:entity,value object和service

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

领域中还有一些方面适合用动作或操作来标识,这比用对象标识更加清楚。这些方面最好用service来标识,而不应把操作的责任强加到entity或value object上。service是应客户端请求来完成某事。在软件的奇数层有很多service,在领域中也可以使用service,当对软件必须实现的某项无状态的活动进行建模时,就可以将该活动作为一项service。

5.1 关联

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

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

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

1)规定一个遍历方向

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

3)消除不必要的关联

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

美国-总统,双向,一对多的关系。乔治·华盛顿,很少会问"他是哪个国家的总统?"。从实用的角度讲,可以将这种关系简化为从国家到总统的单向关联。这种精化实际上反映了对领域的深入理解,而且也是一个更实用的设计。它表明一个方向的关联比另一个方向的关联更有意义且更重要。也使得Person类独立于基础概念President。

通常,通过更深入的理解可以得到一个"限定的"关系。进一步研究总统的例子就可以知道,一个国家在一段时期内只能有一位总统(内战期间或许有例外)。这个限定条件把多重关系简化为一对一关系,并且在模型中植入了一条明确的规则。1790年谁是美国总统?乔治·华盛顿

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

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

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

5.2 模式:entity(又称为reference object)

很多对象不是通过它们的属性定义的,而是通过一连串的连续事件和标识定义的。

很多事物都是由它们的标识定义的,而不是由任何属性定义的。(身份证号)

客户对象的这些形式都是基于不同编程语言和技术的不同实现。但当接到订单电话时,知道以下事情是很重要的:这个客户是否有拖欠的账务?这个客户是不是已经与jack(一位销售代表)保持联络达好几个星期的那个客户?还是说他完全是一个新客户?

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

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

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

  1. 它在整个生命周期中具有连续性
  2. 它的一些对用户来说非常重要的不同状态不是由属性决定的(什么意思?)

有时标识只有在系统上下文中才重要。

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

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

体育场座位,标识符就是座位号,在体育场是唯一的,还有其他属性比如位置、视野是否开阔、价格等,但只有座位号才用于识别和区分座位。

另一方面,如果座位的分配方式是"先到先坐",那么观众可以寻找任意的空座位来做,这样就不需要对座位加以区分。这种情况下,只有座位总数才是重要的。

1. entity建模

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

image
customerID是customer entity的一个(也是唯一的)标识符,但phone number(电话号码)和address(地址)都经常用来查找或匹配一个customer(客户)。name(姓名)没有定义一个人的标识,但它通常是确定人的方式之一。在这个示例中,phone和address属性被转移到customer中,但在实际的项目上,这种选择取决于领域中的customer一般是如何匹配或区分的。例如,一个customer有很多不同目的的phone number,那么phone number就与标识无关,因此应该放在sales contact(销售联系人)中。

2. 设计标识操作

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

5.3 模式:value object

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

由于模型中最引人注意的对象往往是entity,而且跟踪每个entity的标识是极为重要的,因此我们很自然地会想到为每个领域对象都分配一个标识。实际上,一些框架确实为每个对象分配了一个唯一的ID。

这样一来,系统就必须处理所有这些ID的跟踪问题,从而导致许多本来可能的性能优化不得不被放弃。此外,人们还需要付出大量的分析工作来定义有意义的标识,还需要开发出一些可靠的跟踪方式,以便在分布式系统之间或在数据库存储中跟踪对象。同样重要的是,盲目添加标识可能会产生误导。它会使模型变得混乱,使所有对象看起来都是同一模式。

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

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

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

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

地址是否是值对象

1)对于邮局来说,地址是一个entity

2)对于电力运营公司来说,如果几个室友各自打电话申请电力服务,公司需要知道他们其实是住在同一个地方这一点,在这种情况下,地址是一个entity。换种方式,模型可以将电力服务与"住处"关联起来,那么住处就是一个带有地址属性的entity了,这时,地址就是一个value object

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

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

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

value object所包含的属性应该形成一个概念整体。例如,street(街道),city(城市)和post code邮编不应该是person(人)对象的单独的属性。它们是整个整个地址的一部分,这样可以使得person对象更简单,并使它成为一个更连贯的value object。
image

1 设计value object

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

  • 当节省数据库空间或减少对象数量是一个关键要求时
  • 当通信开销很低时(例如在中央服务器中)
  • 当共享的对象被严格限定为不可变时

value可变的集中因素:

  • 如果value 频繁改变
  • 如果创建或删除对象的开销很大
  • 如果替换(而不是修改)将打乱集群
  • 如果value的共享不多,或者共享不会提高集群性能,或其他某种技术原因
  • 再次强调:如果一个value的实现是可变的,那么就不能共享它。无论是否共享value object,在可能的情况下都要将它们设计为不可变的。

2 设计包含value object的关联

模型中的关联越少越好,越简单越好。如果说entity之间的双向关联很难维护,那么两个value object之间的双向关联则完全没有意义。

5.4 模式:service

有时,对象不是一个事物。

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

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

有时,一些service看上去就像是模型对象,它们以对象的形式出现,除了执行一些操作之外并没有其他意义。这些"实干家(doer)"的名字通常以"Manager"之类的名字结尾。它们没有自己的状态,而且除了所掌握的操作之外在领域中也没有其他意义。尽管如此,我们仍然应该把它们归到service这个类别中,这样可以避免它们与真正的模型对象混淆。

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

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

好的service有以下3个特征:

1)与领域概念相关的操作不是entity或value object的一个自然的部分

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

3)操作是无状态的

当领域中的某个重要的过程或转换操作不属于实体或值对象的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为service。定义接口时要使用模型语言,并确保操作名称是通用语言中的术语。此外,应该将service定义为无状态的。

1. service与孤立的领域层

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

领域层、应用层、基础设施层都有service

场景:银行当客户的账户余额小于一个特定的临界值时,这个程序就向客户发送一封电子邮件。封装电子邮件系统的接口就是基础设施层中的一个service

应用层service和领域层service可能很难区分。应用层负责安排一个通知,而领域层负责确定是否满足临界值。

					将service划分到各个层中
应用层			资金转账服务
领域层			与必要的账户和ledger对象进行交互,执行相应的接入和贷出操作
基础设施层	   发送通知服务

2. 粒度

细粒度的对象可能导致分布式系统中的消息传递的效率低下

细粒度的领域对象可能会把领域层的知识泄漏到应用层中,这产生的结果时应用层不得不处理复杂的、细致的交互,从而使得领域知识蔓延到应用层或用户界面代码当中,而领域层会丢失这些知识。明确地引入领域层服务有助于在应用层和领域层之间报纸一条明确的界限

3. 对service的访问

5.5 模式: module(package)

5.6 建模范式

从一开始,对象建模就在简单些和复杂性之间实现了一个很好的平衡

当将非对象元素混合到以面向对象为主的系统中时,需要遵循以下4条经验规则

  • 不要和实现范式对象。我们总是可以用别的方式来考虑领域。找到合适于范式的模型概念
  • 把通用语言作为依靠的基础。即使工具之间没有严格的联系时,语言使用上的高度一致性也能防止各个设计部分的分裂
  • 不要一味地依赖UML。UM确实有一些特性很适合表达约束,但它并不是在所有情况下都适用
  • 保持怀疑态度。工具是否真正有用武之地?
posted @ 2023-05-07 11:28  LHX2018  阅读(36)  评论(0编辑  收藏  举报