各种抽象定义了领域模型。如果有人要求你列出个人银行领域的一些元素,你可能会说出诸如银行和账户之类;账户类型,如支票、储蓄和货币市场;以及交易类型,如借方和贷方。但是您很快就会意识到,许多这些元素与它们是如何创建的、经过处理的业务流程以及最终被逐出系统的方式是相似的。例如,考虑一个客户帐户的生命周期,如图1.3所示。银行创建的每个客户帐户都通过银行、客户端或任何其他外部系统的某些操作来传递一组状态。

每一个account(账户)都有一个在系统内的整个生命周期内管理的标识。我们将这种元素称为entities(实体,区别于值对象)。对于一个账户,它的标识符是它的帐号。它的许多属性在系统的生命周期中可能会发生变化,但是账户却总是在每次打开它的时候,被已经分配给他的账号所标识,不会改变。两个账户可以有相同的名字,也可以有相同的属性,即使这样也会被认为是两个不同的实体,因为他们的账号是不同的(其实类似于我们为实体创建的ID主键)。

PS:图片1.3下面的英文翻译

客户帐户生命周期中的状态。从一个状态转换到另一个状态取决于在早期状态中执行的操作。

每个账户都可能有一个address(地址)----帐户持有人的住宅地址。地址是由它所包含的值唯一定义的。你改变了地址的任何属性,那么它就变成了一个不同的地址了。你能辨别出account(帐户)和address(地址)之间的语义差别吗?address是没有任何标识符的;它完全基于它所包含的值来标识。毫不奇怪,我们称呼这样的对象为value object(值对象).区分实体和值对象的另一种方法是,值对象是immutable(不可变的,java的String就是一个不可变对象),您不能在创建它之后更改一个值对象的内容后,而却不改变对象本身。

实体和值对象之间的区别是领域建模中最基本的概念之一,您必须对这个概念有一个清晰的理解。当我们谈论一个帐户时,我们指的是账户的具体实例,该账户实例带有一个账号标识符,账户持有者的名字,还有一些其他的属性。其中的一些属性组合在一起形成了账户的唯一标识。通常,帐号是帐户的标识属性。即使你有两个帐户,它们的非标识位的属性有相同的值(比方说是账户的持有者的名字或者是账户的开户时间),如果帐户号码是不同的,那么它们还是两个不同的帐户。一个account(账户)就是一个entity(实体),它有自己特定的标识符(就是账号),但是,对于一个address(地址),您只需要考虑值部分。只有value(值)才是最重要的。您可以在实体中更改某些属性的值,但是标识不会改变;例如,您可以更改一个帐户的地址,但是它指向相同的帐户。但是你不能改变一个值对象的值;否则,它将是一个不同的值对象。因此,一个值对象是不可变的。

PS:实体和值对象的不变性语义

当我们在本章后面讨论实现时,我们将对实体和值对象的不可变性有不同的看法.在函数式编程中,我们的目标是尽可能地建模为不可变性----您也可以将实体建模为不可变对象。因此,区分实体和值对象的最佳方法是记住一个实体有一个不能改变的标识,而一个值对象有一个不能改变的值。值对象在语义上是不可变的。而实体在语义上是可变的,但是您将使用不可变结构来实现它。

那么我们对于具有可变语义的实体,以不可变结构对其进行建模,这有什么坏处呢??让我们面对它吧----可变的引用更加高效。在许多情况下,与不可变的数据结构一起工作,相比于直接的可变性结构,不可变结构会导致更多的对象被实例化,特别是当一个领域实体频繁变化的时候。但是,正如您将在本文和后面的章节中看到的那样,可变数据结构导致了脆弱的代码基础,并且在并发操作的情况下使理解代码变得困难。所以一般的建议是从不可变的数据结构开始----如果你需要使代码的某些部分比你的不可变性更有性能,那就突变为可变结构吧。但是要确保客户端API不会看到这个突变;将这个突变封装在一个引用透明的包装器函数后。

PS:作者对于最后一句话的注释

例如,看看Scala集合API的实现。他们中许多,比如List::take或者List::drop,它们在hood(翻译成引擎盖,实在是没语感。Scala的drop方法的实现,借助了可变的列表ListBuffer进行数据的收集与复制,并最终转换为不可变的List返回,应该像表达的就是将突变封装在包装器内,使得客户端无察觉。)下使用突变,但是客户端API没有看到它。客户调用端返回一个不可变的List。

PS:函数的引用透明referentially transparent

即函数的作用,无副作用,比如说我们定义的函数A,调用A,并获取结果return数据,那么引用透明的意思就是我们可以直接以return的值,来替换掉对整合函数的调用的话,那么这个函数就是引用透明的。下面我们举个例子:

def A:Int(x:Int,y:Int) = x + y

//1.0

val result = A(1,1)

//2.0

val result = 2

对于A函数,我们在1.0中调用它,得到结果,这函数结束后,返回一个新的值,我们以result引用它。而其实,步骤1.0所做的操作,我们可以以步骤2.0替代,而且代码也不出错。这就说明函数A具有引用透明。

任何领域模型的核心都是不同领域元素之间的行为或交互的集合。这些行为的粒度比单独的实体或值对象的粒度要高。我们认为它们是模型提供的主要服务。让我们来看一个来自银行系统的例子。比如,一个客户来到银行或ATM机,并在两个账户之间转账。这个操作导致从其中一个账户取出钱,另一个账户存进钱,这将反映出各自账户余额的变化。验证检查必须完成,例如,确定帐户是否是激活状态,以及转出帐户是否有足够的资金转移。在每一个这样的交互中,可以包含许多领域元素,包括实体和值对象。在DDD中,您可以将整个行为模型建模为一个或多个services(服务,我这里提醒一下,不是分层结构的service,那叫应用服务,这里所指的是领域服务)。根据模型的体系结构和特定的有界上下文,您可以将其打包为独立的服务(你可以给他命名为AccountService)或者作为名为BankingService的一个更通用的模块的服务集合的一部分。

领域服务与实体或值对象不同的主要方式是粒度级别。在领域服务中,多个领域实体根据特定的业务规则进行交互,并在系统中交付特定的功能。从实现的角度来看,服务是一组函数,作用于相关的领域实体和值对象。它封装了一个完整的业务操作,该操作对用户或银行具有一定的价值。表1.1总结了迄今为止您所看到的三个最重要的领域元素的特征。

领域元素 特性
Entity 1.有一个标识符 2.在声明周期中经历多个状态3.通常在业务中有一个明确的声明周期
Value Object 1.语义上不可变 2.可在实体间自由共享
Service 1.比实体或值对象更宏观的抽象2.涉及多个实体和值对象3.通常是业务的一个用例模型

图1.4说明了这三种类型的领域元素在一个来自于个人银行领域的示例中是如何关联的。这是DDD的基本概念之一;在继续这段旅程之前,确保你了解了基本知识。PS:图片1.4下方的英文翻译:

模型的领域元素之间的关系。这个例子来自于个人银行领域。注意,account(帐户)、badk(银行)等都是实体。一个实体可以包含其他实体或者值对象。服务处于更高的粒度级别,并实现涉及多个领域元素的行为。

领域元素的语义和限界上下文:

让我们以一个重要的概念来结束对各种领域元素的讨论,这个概念将它们的语义与有界上下文联系起来。当我们说一个address(地址)是一个值对象时,它只在被定义的限界上下文的范围内是一个值对象,你不需要用它们的标识符来追踪address(地址)。但是,让我们来考虑另一个实现了地理编码服务的限界上下文。在那里,您需要通过纬度/经度跟踪地址,并且每个地址可能必须被标记为唯一的ID。address(地址)在这个有界的上下文中成为一个实体。同样地,account(帐户)可能是个人银行应用程序中的一个实体,而在投资组合报告的限界上下文下,您可以将一个account(帐户)作为一个仅需要打印的信息容器,从而实现为一个值对象。领域元素的类型总是反映其定义所在的限界上下文。

results matching ""

    No results matching ""