函数式与响应式的领域模型<Functional and Reactive Domain Modeling>(三)

来源:互联网 发布:二手交易软件排名 编辑:程序博客网 时间:2024/06/16 19:53
本章包含:>设计领域模型---以函数和代数的方式>将领域的代数与它的实现解耦>在api设计中执行代数法则>实现领域对象的生命周期模式前几章介绍了函数式编程与数学的相似之处,尤其是代数。您研究了代数数据类型、sum type(和类型)和product type(积类型),然后您学习了如何将它们组合起来形成抽象的领域元素。这一章将这一讨论带到了下一个层次;您将从模型的规则开始,使用代数的类型组合,为我们的域模型构建api。这些api是遵守领域法则的契约,您将使用类型代数来确保它们是正确构造的。您将学习如何按照代数规范来开发api,并编写能够验证形成业务规则的法律的属性。您已经看到了领域对象的三个基本生命周期模式:聚合,工厂,仓储。本章将介绍如何在您的领域模型中使用函数式编程的基本术语来实现它们。图3.1演示了如何在各个部分中将会取得的进展,并提升以函数技术来构建领域模型的能力。

这里写图片描述

3.1 代数式API设计

在面向对象的(OO)开发中,使用Java之类的语言,您可以使用接口启动API设计,最终将模型的契约发布到最终用户手中。当您准备好接口时,就可以使用类和对象启动具体的实现了。首先确定类,然后将一些操作作为该特定类的方法进行分组。在函数式编程中,这种模式彻底翻转了----您从与基本领域行为相对应的操作开始,并将其分组进相关的模块中,而这些模块是可以通过组合形成更大的用例的。每个行为都使用对类型进行操作的函数来建模;类型代表的是领域的数据、类或对象。在下一节中,您将基于此功能范例设计一个领域服务。在函数领域模型中定义的一个模块,它是一系列函数的集合,这些函数在一组类型上操作,并严格遵守一组称为领域规则的不变量。用数学术语来说,这就是所谓的模块的代数。如果你不熟悉代数的正式定义,我将在这里定义它并解释每一个元素如何映射到我们所称的我们的模块的代数中。>One or more sets:对我们而言,集合是构成模型的一部分的数据类型>One or more functions that operate on the objects of the sets:对我们而言,这些将是您定义并发布为用户的API的函数。>A few axioms or laws that are assumed to be true and can be used to derive other theorems---当您在API中定义操作时,法则将指定这些操作之间的关系。注意,我们还没有讨论实现,这不是代数的一部分。它是行为发布的契约,它构成了API的代数。

3.1.1 为什么一个代数方法?

您可能想知道这种代数方法对API设计的好处。API是一种必须以用户为中心的东西。用户对领域的理解首先是一组行为,这些行为的代数方法在任何类或对象的阴影下都没有云。用户总是首先是通过行为集合来理解一个领域,而这些行为是以没有副作用的纯函数构造的代数方法模型。在您清楚地定义函数之后,您将为用户发布一个清晰的蓝图,告诉用户这个模型可以做什么。对于用户,我指的是那些将使用API并开发更大的抽象的人。这就给我们带来了复合性的主题-----api也需要组合。这是代数方法比其他方法更好的另一个问题。最后,代数中有一套你可以验证的法则。这使您的模型更加具有可验证性。让我们稍微详细地看看这些优势:>Loud and clear----代数方法在一开始就关注模型的行为。行为是用户所看到的,而实现这些行为的函数,则是您作为一个建模者,用来构成领域模型设计的。你没有任何对象或类的认知负荷----只是纯粹将函数组合在一起,作为实现更大行为的模块。>复合性-----一个函数有一个代数。当类型对齐时,函数组合-----你在前面见到过。当您有一个API作为函数时,您可从代数本身通过组合来构建更大的函数。你不需要知道任何一个组合函数的实现的细节知识。对于传统的面向对象思想中的类和对象,在类级的复合性不是一个定义良好的操作。>可验证性----通过定义代数的法则,你实现了你的模型的可验证性。这使您的模型健壮。您在核心模型实现中包含的属性集可以确保在任何阶段都不违反规范中的某些更改。因此,这次讨论的主要内容是理解基于代数的设计理念。一个代数是一组类型的组合,一组用它们定义的函数,以及一组使函数相互关联的法则。请注意,我还没有提到类的具体实现或构成模型API的函数。最初的焦点完全放在了API的代数和复合性方面。这和你在代数中做的很相似----如果你有y=f(x)和z=g(y),你总能得到一个复合函数g(f(x)),而不管f、g、x或y的具体表示。实现是在生命周期的后期进行,作为你的代数的解释器的形式出现。因此,您有一个定义领域API的代数,以及可能有许多解释器来定义单独的具体实现。

3.2 为领域服务定义代数

让我们从一个基于代数的API设计的例子开始,该例子为我们的个人银行领域的一个功能子集而设计的。您将保持初始设计的足够简单;您常常忽略可能的复杂情况,稍后再回来修复它们。让我们从一个模块开始,它定义了一个AccountService的契约,其中有一些您计划支持的操作。在这样一个模块中,您可以使用哪些示例操作?请牢记我之前所提及的,这不像基于类的面向对象方式,你的操作将主要的在模块内。这就使得你的核心领域对象保持lean and thin(简洁不臃肿)。行为不再与对象相结合,并且可以独立地使用对象的类型作为它们所代表的代数的契约进行独立发展。通过与对象本身分离,这种设计可以提高函数的可重用性,稍后您将看到。一个可能的操作是打开一个帐户,该帐户启动了领域模型中的实体帐户的生命周期。让我们定义一个带有你需要的参数的open函数,实现打开一个账户的功能:
def open(no: String, name: String, openDate: Option[Date]): ???
为了让事情变得简单,我们为open函数只带了几个参数---在现实中,将会有更多参数。但是函数的返回类型应该是什么呢?它可以是一个Account类型,指示刚刚打开的帐户。但是,由于验证失败,open函数可能会失败,作为函数编程世界的一个好公民,您不会抛出异常,并期望用户能够捕捉到这些异常。在这种情况下,返回一个可选的的数据类型,例如Option,可能是有意义的。如果与此同时,您希望告知用户在发生故障时发生了什么错误,您需要一个数据类型来保存此信息。这里有几个选择。您可以从Scala标准库中使用Either(作者注:Either是另一个模型分离的代数数据类型。 Either[A, B]包含类型A的值(也称为左投影)或者类型B的值(也称为右投影))或者Try。但是,从open函数中返回一个Account,和截然相反的返回提供了了评估的抽象的Option,Try,Either,你能识别出基本差异吗??这听起来有点复杂,因此,让我们更深入地了解一下函数式编程是如何在评估中提供抽象的技术,从而形成更好的组合性。

3.2.1 建立在评估上的抽象

在本文中,我将介绍一个在讨论领域行为的组合时的一个重要的概念,这个概念会贯穿全书!!当你有一个数据类型Account作为open函数的返回值的时候,在你函数操作成功后,将会返回一个实例化额Account对象。但是一旦他失败了,你可以返回一个null,或者考虑抛出一个异常。所有的上述情况,你所返回的计算的值都是被评估过后的,而你返回的是评估后的结果。现在让我们将这种情况与返回像Option/Try这样的数据类型做一个比较。严格来讲,Option和Try并不是数据类型,它们是类型构造器,它们建模了一个有效的计算。Try抽象了失败的影响,而Option则建模了可选择性的影响(这意味着你不必拥有一个值)。当你返回一个Try或者Option的时候,您正在返回一个建立在评估之上的抽象。您的函数的调用者可以决定是通过提取必要的值来评估抽象,还是用其他抽象来组合它。这样做的好处是可以获得更多的组合性,就像在下一节中所讨论的示例中所看到的那样。请参考下面的侧栏“Try as an abstraction”来获取更多的详情,理解为什么Try步进是一个处理异常的方法,而且提供了一种抽象,让您可以在评估处理失败之前通过组合构建更大的抽象。

3.2.2 组合抽象

现在你已经看到了,open函数返回一个抽象是要优于返回一个具体的值,您需要确保的是,你的这个抽象可以去其他的相似的函数组合在一起。考虑一个您需要在一个帐户上执行的 操作序列,比如打开账户并执行一系列的debits(取款)和credits(存款)操作。这看起来像是在命令式编程中所做的操作序列。但是在函数范式中,您想要给用户一种强制排序的感觉,同时在底层进行面向表达式的编程。这就是函数的返回抽象在其中扮演的重要角色。当您有一个操作序列时,在成功的情况下,每个返回的内容都需要进行排序,并生成最终的计算。如果其中任何一个操作失败,整个组合操作需要报告一个失败。在Scala中,您通常通过使用“for语句”来实现这一目标:
for {    a <- open(..)    b <- credit(..)    c <- credit(..)} yield(..)
图3.2显示了组合需要如何处理我们讨论过的计算结构。抽象需要支持在成功的情况下可串联,同时允许在任何失败的情况下中止操作的组合。图3.2演示了计算的monadic模型,在进行顺序操作调用时,您将使用它来链接AccountService的方法。每个服务函数的返回类型都需要支持这个monadic模型。这就是你用来组合代数的策略。当你在要将你的操作链接起来的地方使用这种方法,你也会明白当我说一个计算就是一个monad的意思了。这里的计算发生在一个抽象语义的结构中----你为AccountService API使用Try来抽象失败的语义。我称这种计算是effectful,而monads是一种组合effectful计算的方法。在第四章当我们讨论函数式编程的其他模式时,您将学习更多关于此类模型的知识。

这里写图片描述

Try是在Scala标准库中定义的monad。Try实现了flatMap,它允许对我们刚刚所讨论的影响进行排序。使用Try的组合功能,您可以定义由原始的操作组成的操作,正如你马上就会看到的从debit和credit来实现的transfer操作。到目前为止,你应该对如何使用monad来组合代数的代数方法有一个很好的认识。从计算上来说,一个monad是一个抽象的计算,它支持一定数量的操作。第四章将monads作为领域模型设计的通用计算结构详细介绍。PS:Try as an abstractionopen函数返回类型是Try[Account]而不是Accouny的好处是什么呢??当你返回一个实例化的Account类型的对象时候,你返回已经被评估的东西,这个值是不可以组合的。如果你返回了一个Try[Account],你返回的是对评估结果抽象了的东西。这个结果可以是Failure,也可以是Successs,但是这个抽象让你可以用代数方法把返回的值和其他抽象组合在一起。看一下transfer函数实现的组合计算。

3.2.3 The final algebra of types

既然您已经了解了如何定义示例领域行为的代数,那么让我们看一下AccountService发布的类型的完整代数。我们将以一个示例来结束本节,该示例仅使用组合方法的代数,将用较小的服务方法编写一个更粗糙的服务方法。在这个模块中你可以使用的其他的操作是close(关闭一个账户),debit(从账户中取款),credit(向账户存款),balance(返回当前账户额可用余额)。在定义这些操作时,您已经介绍了几种类型,它们从语义上讲的话是领域的语言。现在你不知道这些类型会是什么样子,我还没有说过你将如何实现这些类型。让我们对这些类型的 AccountService模块进行参数化。正如我先前讲的,这就是这个模块和api定义的代数的精确目的----从实现中解耦契约。下面的清单显示了该模块及其操作。

这里写图片描述

每一个函数都返回一个Try。正如我们之前提到的,这样做有两个原因。首先,它帮助将失败信息传递给用户。其次,它可以帮助你进行操作排序,因为Try是一个monad。这里有一个例子,说明如何使用debit和credit,组成一个新的函数---transfer.
def transfer(from: Account, to: Account, amount: Amount):    Try[(Account, Account, Amount)] = for {        a <- debit(from, amount)        b <- credit(to, amount)    } yield (a, b, amount)
很好!您已经只通过代数本身,定义了一个业务操作的完整实现。函数transfer的代数由它所接收和返回的类型组成,并定义领域行为的业务契约。请注意,当您谈到代数时,您只讨论构成契约的类型,而不是实现的类型。你不知道你的Account和Balance的类型会是什么样子或者你如何实现debit或credit行为。然而,你已经完全实现了一个由它们组成的函数。这就是代数组合;你可以把之前的函数组合起来,因为代数的类型对其的很完美(我觉得他想说的其实是每个函数的返回值与下一个函数的参数,总是可对应的)。注意刚刚你是如何指定你想要在复合函数中做什么的(作者称这是what部分,即做什么)。Try数据类型的魔力则是关注在how方面上。实际发生的事情是你的返回Try的monadic API将操作绑定在一起了。“for”的语法糖给了你一种命令式的感觉,在内部保留了面向表达的编程的所有好处。现在,您可以将transfer函数包含到模块定义的一部分中,因为它不依赖于任何函数的实现。PS:what's a monad??在函数编程中,你将计算声明为表达式。一个表达式可以是一个原始的计算,或者一个复杂的运算,这个复杂的运算是由于将简单的函数粘在一起而演化而来的。当您将领域行为作为领域模型的一部分构建时,您将实现以类似方式演进的组合器。您从简单的函数开始,然后使用高阶函数的强大功能,组合它们来设计更大的抽象。monad抽象了位构建这样的组合库的一种计算风格。一个monad抽象包含下面这三个元素:>一个类型构造器,M[A],在Scala中以trait M[A]或者case class M[A]或者是calss M[A]的形式来表达。>一个将计算提升进monad的unit方法。在Scala中,为此,您可以使用类构造函数的调用。>一个bind方法,即对计算排序。在Scala中,flatMap就是这个bind这意味着一个monad是一个代数结构。任何有这三种元素的monad都需要遵守以下三条定律:>Identity---对于一个叫m的monad, m flatMap unit => m>Init----对于一个叫m的monad, unit(v) flatMap f => f(v)>Associativity---对于一个叫m的monad,m flatMap g flatMap h => m flatMap {x => g(x) flatMap h}

3.2.4 代数的定律

正如我之前提到的,基于代数的API设计的一个步骤是将代数的定律形式化。您需要显式地记录您的api必须遵守的一些不变量。这些可以是通用的约束,也可以由领域的规则驱动。让我们考虑一个样例。你应该强制遵守的定律之一是:对于所有的帐户,给定一个余额B,一个成功的存款和一个相同数量的取款操作之后,将还会返回B。使用我们的API定义,您可以将该定律作为您的模型的一个属性来形式化,并将其记录为您的测试套件的一部分。在第9章讨论基于属性的测试时,您将探索如何验证模型的不变量。以下是该定律的一个转化样本:
property("Equal credit & debit in sequence retain the same balance") =    forAll((a: Account, m: BigDecimal) => {        val Success((before, after)) = for {            b <- balance(a)            c <- credit(a, m)            d <- debit(c, m)    } yield (b, d.balance)    before == after})
这个代码片段使用一个名为ScalaCheck的库,它是Scala中基于属性测试的一个很受欢迎的库。您将在第9章学习这个工具。要从这个讨论中分离出来的重要一点是,通过使用这些不变量并将它们编码为可验证的属性,您不仅可以记录领域模型约束,而且每次构建系统时都要执行它们----这是基于代数的API设计的另一个重要方面。

定律使聚合保持一致性

如您所知,聚合的最重要属性是它定义了抽象的一致性边界。无论您在聚合上做什么操作,它从模型的角度都不会变得不一致。你的代数定律必须确保这一点。许多定律将通过类型系统进行验证,而其他的则可以通过在3.2.4节开始讨论的基于属性的测试来进行检查。在清单3.1的示例模块中,一致性保证之一是当您关闭一个帐户时,关闭日期必须大于帐户的打开日期---这意味着,模块AccountService实现的帐户上的任何操作都不可能违反这个不变量。最明显的一个功能是关闭操作。所以,作为模块必须遵守的定律的一部分,你必须有一个在账户聚合上强制执行这个规则的人。。你将会在下一节中实现这个模块和close操作的时候,看到应该去实施。练习3.1 建模失败的多种方式在本节中,您使用Try作为模型失败的计算结构。让我们探索其他选项,找出方法的相对优点:>通过使用Option作为方法的返回类型重写AccountService的定义。提示:Option也实现了flatMap,可以用于链接计算。你认为Option是一种更好的方式来建模你的用例,还是Try呢??>使用Either来模拟操作的失败。和Try一样,Either也是一种和类型,可以用来分离成功和失败的分支。当将操作串联进序列中时候,Either回事一个比Try好的选择吗??

3.2.5 代数的解释器

前一节中定义的代数为您的域模型的API的契约进行了总结。您现在可能想知道,API的实现如何呢?其思想是将实现与代数本身分离,这样就可以让单个代数有多个实现。每个实现都被称为代数解释器,它将由具体的类和实现API定义的函数组成。请记住,当您定义了api时,您引入了一些子虚乌有的类型(例如,Amount,Account,Balance),并在清单3.1中使用它们来行参数化AccountService。现在是为每种类型提供具体实现的时候了,如下面的清单所示。清单3.3提供了一个针对清单3.1定义的代数的示例解释器(其实就类似于对接口的实现)。

这里写图片描述
这里写图片描述
这里写图片描述

图3描述了如何将代数与解释器分离。你也可以为一个代数,生成多个解释器。对于所有实现细节和完全可运行的代码,请查看这本书的可下载代码。

这里写图片描述

3.3 领域模型的生命周期中的模式

第1章介绍了构成域对象生命周期的三个阶段:>Creation---对象是由它的组件创建。>Participation---一个有效的领域对象与其他抽象交互以交付领域内的功能。>Persistence----一个领域对象被写入到某个持久化存储中。当我们讨论使用任何技术实现的领域模型时,这些都是通用模式。让我们将这三个生命周期阶段作为个人银行领域中最重要的对象----客户账户。当你走到最近的银行,请求友好的出纳员为你开一个账户时,底层的银行系统会为你创建一个 Account对象。它必须是一个有效的对象,包含作为帐户的持有者的您的详细信息,然后系统将一个有效的帐号作为唯一的身份标记分配给它。创建帐户后,它将参与各种与帐户相关的交易(比方说取款,存款,转账等),这些交易直接影响您的帐户的状态和余额。它还可以参与各种后台操作,比如把兴趣贴在你的账户上。所有这些都是participation阶段的例子,其中领域对象是银行业务活动的一个内在的聚会。您的帐户细节不能总是驻留在系统的易失性内存中----有时您需要在存储中持久化帐户和交易,当然也要提供查询、更新和删除功能。所有这些生命周期模式都与所有领域对象的聚合相关,这构成了系统的整体模型。本章介绍了一些使用函数式编程技术和Scala惯用语的实现。你可以以多种方式创建一个对象本章介绍了使用一般的函数编程技术对这些模式的实现以及Scala的一些习语。你可以以多种方式创建一个对象---最简单的方法是直接调用类的构造函数。但是最简单的技术总是有一些缺陷,而且在几乎所有的重要的用例中,它也可能看起来很幼稚。在为您的领域模型设计任何创建型模式时,您需要应用适当的软件工程原则。创建过程也需要进行验证----你的对象在创建的时候需要最低限度的有效。你们很快就会明白我的意思。对象经过领域验证阶段之后,您得到一个完全创建的有效的领域聚合。正如你第一张所学的,聚合是描述领域模型的中心概念的完整领域实体。第1章中的例子是一个帐户,它无疑是您的模型中最重要的参与者之一。(如果你需要复习一下有关领域的知识,请休息一下,再复习第一章,了解更多细节。)。验证是一个重要的主题----验证需要对对象是可插拔的,并在各种约束下可重用,并且需要具有特定的失败语义和对领域友好的语法。你会看到函数组合如何为上述所有的这些条件提供一个完美的配方。我们将在本章后面详细讨论domain validation pattern(领域验证模式)。现在您已经有了一个聚合(这是一个有效的领域实体),您一定想知道是什么构成了聚合的内部实现。基于类的OO技术已经教会了您如何在相同的模块结构中对数据元素和函数进行耦合,并提出了领域元素的chubby-looking(直译是看起来胖乎乎的,表达的意思就是模块的臃肿)分层结构。OO爱好者将其称为rich domain model(富领域模型,叫胖领域模型也行);丰富的程度直接与结构和功能的数量成正比,在同一类层次结构下,您可以将它们组合在一起。在函数式设计中,我们讨论的是瘦的领域对象-----您只需要对于描述抽象的核心方面所必须的一定数量的结构进行装配即可。您将使用代数数据类型来建模我们的瘦元素。The functionality will be distributed across reusable and extensible structures called modules that will form the algebra of the model(该功能将分布在称为模块的可重用和可扩展的结构中,这些模块将构成模型的代数)。每个模块大致相当于一个业务功能单元---比如一个Portfolio模块可以处理和报告客户证券投资组合的各种功能。这种模块化的方法使得大型模型可以通过结构化的方式来理解,这让用户感觉更直观。另外,如果您的语言支持将模块作为头等值,那么您可以通过编写更小的模块来构建更大的模块,并鼓励模型功能的有机演化。这反过来又使您的模型更具可重用性和可维护性。一个聚合体参与到各种领域行为中。你将学习如何用抽象的方式来建模这些行为模式,让你可以有效地重用它们,并常常可以在数学上证明它们的正确性。这些抽象的大部分都是lawful(合法的,即符合定律的);他们将遵守某些定律,防止他们在其领域生命周期的任何阶段出现不一致。您将确保它们是正确的,并使用类型系统的强大功能作为对正确性的保证。您当然还会扩率持久性模式----主要从分离持久化和领域的主要工作流程的角度来看。您不会探究特定的数据库问题或技术,但是您将确保您的实现是通用的,即可以与许多持久性存储进行交互。让我们从一些与领域对象创建相关的模式以及如何向用户呈现一个统一的接口,用于创建相关模型元素的家庭来开始。

3.3.1 工厂—对象产生的地方

不客气地说,如果您处理的是基于类的语言,您可以使用类的构造函数来创建对象。Scala是一种基于类的语言,但是您将使用它的函数抽象来创建----代数数据类型,以样本类形式实现。在前面的章节中,您已经看到了用样本类构造对象的例子。在本节中,您将使用大型领域模型丰富创建的语义。模型越复杂,您就越需要管理您的抽象。这是为您的领域元素(我们第一章将实体和值对象归纳为领域元素这个术语)采用特定的创建策略的驱动因素之一。Factories(工厂,本书的作者参考了DDD这本书的称呼)为创建对象提供了一个统一的接口,但是术语factory只是模式词汇表的一部分----它是这种模式的实现,它在编程语言和范例之间有所不同。Scala提供了样本类作为一种建模不可变领域元素的方法,而样本提供了一个免费的伴生对象,它是用于创建的默认工厂。您可以使用它们来创建域元素。但是,当我们谈到要大规模的部署和管理复杂模型时,还有很多其他事情需要考虑:>你是如何确保工厂返回个客户端的的是一个可用有效对象>验证逻辑应该到哪里去?>当验证失败的时候会发生什么----你会抛出异常吗??当时如你在第一张所见,异常违反了引用透明。你必须更聪明地构造对象。您的工厂需要返回一个最低限度有效的对象,这意味着它不能有无效或不一致的核心组成部分。这里可以有一些详细的业务验证,这些验证还没有被执行,但是像年龄为负数这样的基础规则是不被允许的。

3.3.2 The smart constructor idiom

允许简单的构造对象,并且该对象需要遵守一组约束条件的标准技术,通常被称为smart constructor(“智能构造函数”)。您禁止用户调用代数数据类型的基本构造函数,代之提供了一个更智能的版本,它可以确保用户返回一个数据类型,从这个数据类型中,她可以恢复一个有效的领域对象实例,或者对失败进行适当的解释(其实就是Option嘛)。让我们考虑一个例子。在我们的个人银行领域,许多工作可能需要安排在每周特定的日子里执行。这里有一个抽象概念----你可以实现一周内的一天,这样的话您可以将它作为构建过程的一部分进行验证。你可以把一周的一天作为一个整数值,但是很明显它需要遵守一些约束条件才能使它成为一周有效的一天。它必须是1到7之间的值,1代表一个星期一和7代表一个星期天。你会做下面的事吗?
case class DayOfWeek(day: Int) {    if (day < 1 or day > 7)    throw new IllegalArgumentException("Must lie between 1 and 7")}
这是违反我们的引用透明性的首要标准----异常并不是函数编程中的公民。下面的清单说明了这个抽象的智能构造函数。看一下代码,然后我们将分析它以确定其基本原理。

这里写图片描述

让我们来探索一下这个实现所提供的一些特性:>创建一个DayOfWeek的主界面被命名为unsafe(不安全的),并且标记为私有的。它没有公开给用户,只能在实现中使用。用户不可能通过使用这个函数调用返回DayOfWeek的实例。这是有意的,因为如果用户将一个超出范围的整数作为参数传递给这个函数,那么这个实例可能不是有效的。>获取数据类型的实例的唯一方式就是使用dayIfWeek方法,这个方法是出自伴生对象 DayOfWeek。>请注意智能构造器的返回类型,返回值类型是Option[DayOfWeek],如果用户传递了一个有效的Integer值,那么将获取Some(DayOfWeek)的返回值,或者是一个None,代表的是缺少值。>为了保证样例简单,Option被用于表示已构造实例的可选存在。但是对于可能具有更复杂的验证逻辑的数据类型,您的客户可能想知道对象创建失败的原因。这可以通过使用更富表现力的数据类型来完成,例如,Either或者Try,它允许在创建失败时候返回原因。在下一个示例中,您将看到它的说明>创建和验证的大部分领域逻辑都从核心抽象(指的是trait)转移到伙伴对象(即模块)。这就是我所说的瘦模型实现,而不是OO所支持的富(胖)模型。>智能构造器的一个典型的调用可以是DayOfWeek.dayOfWeek(n).foreach(schedule) ,这里的schedule是一个函数,对DayOfWeek从输入中获取的job作业进行调度。本节以我们域的另一个例子结束,它涉及到相关对象的创建。您将为创建函数使用更有表现力的类型,它可以在创建失败的时候将错误信息也返回给调用者。PS:什么是闭合trait??在Scala中,如果trait或class的所有子类型都在同一个文件中,那么trait或class就会被密封,就像我们上面例子中所定义的那样。当您将一个特性封装起来时,编译器就会知道它可能拥有的所有可能的子类型,因此可以对此进行推理。例如,如果您在模式匹配中使用该特性,并且不包含匹配中的所有子类型,编译器可以发出警告,说明匹配不是详尽的。为领域元素使用一个封闭的特质,可以使子类型的数目预先固定(实在不理解的话,你就想成是一个枚举)。

3.3.3 用更富有表现力的类型来使得创建更智能

你可以创建多种类型的账户----向支票账户,存储账户---创建和验证逻辑仍然通过模块系统中的智能构造器进行。看一下清单3.5;如您所见,它与清单3.4的实现略有不同。

这里写图片描述
这里写图片描述
这里写图片描述

我们在前面讨论了智能构造函数的概念,并详细介绍了来自我们个人银行领域的两个例子。现在您已经清楚地知道了这种技术看起来很有价值的用例。以下是总结这一有用性的主要结论:>当您需要实例化需要遵守一组约束的领域对象时,智能构造函数的语法非常有用。>类的构造函数需要从用户那里得到保护,因为直接使用它可能导致不安全且不一致的创建。类声明中的private说明符阻止了样本类的直接实例化,而final关键字阻止了继承。>已发布的API需要显式地说明失败情况,而返回的数据类型必须对用户具有足够的表达能力。>对于不需要显式验证的简单对象,可以直接使用类构造函数。使用具名实参使构造函数调用具有表达性在本节中,您探讨了在领域对象的生命周期中经常出现的一般模式。您了解了创建模式和示例实现。您将在后面的部分中看到其他模式,如聚合和存储库。您将了解到实现与您在OO编程中遇到的类似结构的不同之处。我向您保证函数式编程可以有更好的重用性,您将看到如何使用可重用、可扩展和易于测试的功能技术实现聚合和存储库。

3.3.4 具有代数数据类型的聚合

如前所述,一个聚合为外部世界提供了到一个实体的单一的引用点。当你将Account看做是一个实体时候,这个Account可以包含其他的实体,包括Address或者其他的值对象比如说日期。但是作为API的客户端,您想要操作帐户,而不需要考虑组成Account的其他元素的实现细节。让我们考虑如何实现客户的证券投资组合。一个证券投资组合由一系列的期权组成,这些期权表明客户可能在某一特定日期的各种货币余额。例如,客户John Doe可能在2014年3月26日拥有以下投资组合:>Account E123 has a balance of EUR 2,300.>Account U345 has a balance of USD 12,000.>Account A754 has a balance of AUD 3,456.如何将证券投资组合建模为一个聚合,从而使您的API的客户端不需要处理诸如Position(期权)或Money(金钱)之类的单个元素?这里有一种实现聚合的方法:
sealed trait Accountsealed trait Currencycase class Money(amount: BigDecimal)case class Position(account: Account, ccy: Currency, balance: Money)case class Portfolio(pos: Seq[Position], asOf: Date)
这里你有一个证券投资组合的聚合,并以代数数据类型的Portfolio作为其聚合根。这里还有很多其他的元素----实体类比如说Account,以及值对象比如说Date,Money或者Currency-----这构成了整个聚合的结构.对于客户端,Portfolio是交互的中心和唯一点,您可以通过封装来自用户的证券投资组合的实现的所有细节来保证这一点。聚合根发布的方法只从用户那里获取相关的详细信息,并报告适当的信息:

这里写图片描述

你在这里设计了一种“胖”聚合----它包含多个实体,比如Portfolio(这个是聚合根)和Account(即表示的是谁的证券投资组合正在生成)。在很多情况下,这种设计可能不太大,尤其是如果你有一个大的模型。它包含多个实体,当您有多个实体参与聚合时,就很难在不变量和事务之间执行一致性边界。在这种情况下,您可以一直重构这个Position(期权),使其包含帐户号而不是整个帐户实体。由于帐号惟一地标识一个帐户,所以实现可以在需要时构造一个Account实例。因此,聚合只包含有一个实体,即Portfolio(它是聚合根)以及其他的值对象。在传统的OO开发中行,你会将所有操作Portfolio的相关的函数都放在这个类自身内。但是正如您在本章前面所看到的,在函数式编程中,您希望保持抽象的最小化和精简。Portfolio代数数据类型将会只包含构成证券投资组合的最小结构。所有相关的函数都进入提供API定义的模块中,客户端可以使用这些定义。请参阅第3.1节,讨论如何设计模块的代数来组织领域函数。这次讨论的主要结论是,一个聚合是由(a)代数数据类型,它组成实体的结构,和(b)模块,它提供了以一种组合方式操作集合的聚合的代数。

MODULARITY(模块化)

将聚合的两个部分分离是一个非常重要的概念,您应该在设计领域模型时牢记这一点。这就导致了设计的模块化,并清楚地划分了集合的结构和功能。不要让聚合的实现结构泄漏到客户端。甚至一些函数式编程技术,例如模式匹配有时也会导致实现细节被暴露给客户端。除非有一个重要的原因,确保聚合的所有操作都是通过遵守模块的代数定律。PS:Warning about pattern matching在Scala中经常使用模式匹配。对用户来说,这是一种语法上的方便、表达和可读的功能。但是模式匹配有几个缺点,作为域建模者,您应该知道:1.模式暴露对象表示,因此被认为是反模块化的2.您不能定义匹配的定制模式,它们与case类的类型是一对一的。因此,没有可扩展模式。在许多情况下,您可能需要考虑使用Scala的提取器模式。关于为什么提取器是一种改进的模式匹配的更多信息,请参考Martin Odersky、Burak Emir和约翰威廉姆斯的论文。

INVARIANTS(不变量)

当我们讨论一个模型及其相关元素时,它们的功能都基于领域的规则。无论情况如何,这些规则永远不会被违背;例如,您不能创建两个帐户拥有相同的帐号,也不能在早于开户日期前拥有一个帐户。当您创建聚合时,您的API的职责是确保这些领域规则得到了遵守。可能存在一些复杂的业务规则,您可以在创建聚合后进行验证。但是,基本的验证必须传递给新创建的对象,并且领域必须确定这些规则中的哪些是基本的。第3.3.2节讨论的智能构造器习语是您可以使用的技术之一,以确保在创建时遵守不变量。

3.3.5 Updating aggregates functionally with lenses

到目前为止,我们关注的是不变性和引用透明性----那么现在我们来讨论更新。这难道不是矛盾吗?实际上,更新是一种自然现象,您设计的每个域模型都需要为领域元素的更新提供支持。在Scala中,你有vars,它为你提供不受约束的对象突变。但出于显而易见的原因,我们不会落入这个陷阱----它不会让我们在函数式编程范式中有任何意义。函数式编程中最常见的方法是避免就地发生的突变,而是用更新后的值生成对象的新实例。Scala样本类提供了这个操作的语法糖。这里有一个例子:
case class Address(no: String, street: String, city: String,    state: String, zip: String)
该样本类Address是一个ADT(代数数据类型),它建模的了客户的地址模型。如果你想要修改其中一个属性,在Scala的语法中这很简单:

这里写图片描述

这看起来很cool,而且它也没有违背不变性的公理。但让我们看看这种方法是否有规模性。下面我将将Customer实体介绍给你,并给予它Address属性。
case class Customer(id: Int, name: String, address: Address
您还将使用相同的复制方法更新特定客户地址的no字段。
val c = Customer(12, "John D Cook", a)val nc = c.copy(address = c.address.copy(no = "B152"))
你预料到有什么问题吗?对象的嵌套级别越高,语法就越混乱。想象一下,当您考虑更新一个深度嵌套的对象时,你能想象到copy嵌套等等级吗??这种情况需要更好的抽象----您希望保持不变性的优点,并向用户提供一个合适的API。和前面一样,让我们转向设计抽象的代数方法。在给出抽象的正式定义之前,让我们试着提出这样一个抽象需要满足的代数的需求。我们把这种抽象称为lens(透镜)。1.Parametricity-----透镜需要对你需要更新的对象的类型(我们称它为O)参数进行参数化。因为每个更新都是针对对象的特定字段,所以透镜也需要被更新的字段的类型(让我们称它为V)参数化。这就给了你一个透镜的基本契约:即case class Lens[O,V]()2.One lens per field(每个属性一个透镜)---对于每一个对象,其每一个属性都需要有一个透镜。这可能听起来会很啰嗦,在某些情况下它可能有那样的感觉。稍后,您将看到如何通过使用宏的库来绕过这个繁琐的程序。3.getter---透镜的代数需要发布一个getter来访问该字段的当前值。这仅仅是一个函数:get:O => V4.setter:透镜的代数需要发布一个setter。它应该接受一个对象和一个新值,并返回一个与该字段值相同的新对象,并将该字段的值更改为新的值。很明显它具有一个函数set : (O,V) => O总结这些要点,有一个透镜ADT的实现就像下面这样简单明了:

这里写图片描述

透镜的想法并不是什么新鲜事。本杰明皮尔斯和其他人已经探索了这种数据结构,以解决双向转换问题,比如关系设置(作者注:它可能听起来很复杂,问题在于是在关系数据库中维护一个视图和该视图的底层表之间的一致性,以防更新。看看“Relational Lenses, a Language for Updatable Views”,Aaron Bohannon,地址是http://dl.acm.org/citation.cfm?id=1142399)中的视图更新或树结构数据的转换(作者注:地址是http://www.cis.upenn.edu/~bcpierce/papers/newlenses-full-toplas.pdf)。如果您对透镜的历史感兴趣,作为计算抽象,请参考这些参考资料。本节讨论的实现是一个过于简单的实现。更复杂的透镜包括Scalaz(https://github.com/scalaz/scalaz)和shapeless(http://github.com/milessabin/shapeless)。Gérard Huet的Zipper(http://dl.acm.org/citation.cfm?id=969872)是另一种抽象,允许对递归数据结构进行功能更新。让我们从我们的个人银行领域获取一个例子,看看如何使用一个透镜来实现更新。建设你有一个Address的实例,它建模的是银行客户的地址,而现在你需要修改家庭住址的no属性。假设您有一个用于Address的样本类,那么可以很容易地使用copy函数来完成这个操作。但是现在你有了一个闪亮的透镜抽象,你想用它来进行更新。这听起来可能有些琐碎,但请记住,您的目标是在客户对象的地址中有一个用于嵌套更新家庭地址no属性的API。您想要泛化这个策略,以便它可以扩展任意深度的任意嵌套对象。让我们定义一个透镜,其中对象是类型Address,而要更新的值(no属性)是String类型的:

这里写图片描述

你如何在实践中使用这个透镜?下面是一个关于Scala REPL的示例会话:

这里写图片描述

类似地,您可以定义一个透镜来更新客户的地址字段:
val custAddressLens = Lens[Customer, Address](    get = _.address,    set = (o, v) => o.copy(address = v))
你可以有尽可能多的透镜来更新单个的字段。但我们还没有解决在案例类中copy函数的嵌套问题。在函数式编程中同样如此,函数组合又一次帮助了我们。对于要组合的函数,类型需要对齐。在之前的例子中,custAddressLens将Customer映射到Address,addressNoLens将Address映射到String。它们的类型也反映了相同的映射。现在看到对齐了吗?他就在那等着被你组合呢!!但你不会把它作为这两个特殊透镜的特殊情况来实现。相反,您将定义一个泛型compose函数,这样您就不必为每一对您编写的透镜重复相同的代码。实现的其余部分只是用于定义getter和setter的以下类型:

这里写图片描述

现在您可以使用这个compose函数来创建一个更大的透镜,它可以遍历Customer的嵌套数据,并跳转到更新客户address内的no属性:

这里写图片描述

领域模型中的聚合的目的之一是通过聚合根控制对底层实现的访问。API的客户端不能直接访问较低级的非根聚合元素。这可以通过使用透镜合成来完成。暴露顶层透镜,只允许通过根元素转换低级对象。PS:Using lenses透镜的一个常见问题是决定什么时候使用。Scala中的样本类的copy功能是否足以处理功能更新?和许多其他抽象一样,透镜是可伸缩的。对于ADT中的简单更新,copy函数工作得很好,可能是推荐的方法。但请考虑以下场景:>您需要在深度嵌套的ADTs中执行更新。在这种情况下,copy可能会变得笨拙。在这种情况下使用透镜。>您需要使用其他抽象来组合ADTs的更新。一个常见的例子是与状态monad的组合。状态monad是管理应用状态的一种方法,它可以随时间变化而不使用就地突变。在使用状态monad时,可以有效地使用透镜进行更新部分的功能。你会在第四章看到例子。这是对透镜的简要介绍。这本书的在线代码存储库包含了一个领域实体Customer的透镜的完整实现,你可以回顾它来研究透镜是如何在你的代码库的整体架构中实现的。考虑到一个重要领域实体的透镜实现,您可能会认为它过于冗长,无法为实体的所有属性单独定义透镜,并且想知道是否所有这些冗长的实现都值得的。首先,您可以使用适当的库来摆脱这种繁琐的程序,这些库提供了使用Scala宏实现的透镜。Monocle(https://github.com/julien-truffaut/Monocle)就是一个这样的库。Scalaz和Shapeless也是两个优秀的库,以Scala语言实现函数式编程抽象,并提供透镜作为其数据结构的一部分。

LENS LAWS(透镜定律)

我们的ADT透镜定义可一个抽象的代数。你可能还记得,每一个代数都有一组定律需要去遵守,以确保它的一致性。通俗而言,这里有三条定律,它是每一个透镜都需要遵守的---他们可能看起来是不重要的,但值得用文档记录它们,并在定义新方法时用基于属性的测试来验证它们。>Identity---如果您先获取到值,然后set相同的值,对象将保持不变。>Retention---如果你set设置了一个值,然后get获取该值,那么你获取到的值将时你新设置的>Double set----如果你set了两次,并且都成功了,那么我们get得到的值将会是最后一次设置的值在你的领域模型中,当你定义透镜来更新你的聚合体时,不要忘记去验证透镜是否满足定律。毕竟,聚合定义了模型的一致性边界。作为一个设计人员,您的职责是确保所有的逻辑不变量都可以在您的聚合上执行的所有操作中获得遵守。一旦你学会了如何使用基于属性的测试,您可以验证属性实现的任何透镜。明确地将这些定律作为代码的一部分是一个很好的实践-----毕竟,它们可以作为可验证的和可执行的领域约束。练习3.2验证透镜定律第3章的在线代码存储库包含一个Customer实体的定义和它的透镜。看一下addressLens,它会更新客户的地址,并使用可以验证镜片的法律的ScalaCheck来写属性。

3.3.6 存储库和“解耦”的永恒艺术

第1章介绍了存储库。存储库是聚合生活的地方,尽管形式或形状可能有些不同。但是,您总是可以从存储库中构造一个聚合体。如何实现这取决于存储库的结构以及聚合和存储库实现范例之间的阻抗失配。存储库的另一个功能是,您可以从存储库查询聚合。在本节中,您将学习如何执行以下操作:>使用Scala的特性来设计和实现一个仓储>使用Scala的特质以模块来组织仓储>以一种组合的方式管理向领域服务注入仓储让我们从一个简单的仓储模式的API开始。根据您的模型需求,您可以将仓储组织到模块中。如果模型很小,就可以实现一个通用仓储,并将所有聚集在简单模块内。通常,当您必须设计一个重要的领域模型时,您将发现自己为每个聚合体设计单独的存储库,并将它们作为单独的Scala模块进行组织。对于我们当前的用例,您正在处理Account聚合,并需要一个专用的仓储模块。仓储提供了一些通用功能,允许您执行以下操作:>基于ID获取聚合(记住,一个领域实体可以唯一被ID所标识)>存储一个完整的聚合您将把这些函数抽象成一个通用的模块 Repository,如下:

这里写图片描述

你可以通过继承通用的模块,来生成一个串门处理Account聚合的仓储----AccountRepository

这里写图片描述

请注意,您保留了函数的返回类型为Try,作为与仓储交互失败的抽象。PS:模块化组织仓储当您有多个聚合体时,在单独的模块中为每个存储库组织存储库。使用这种方法,您可以清楚地分离对问题和解决方案领域的不同实体进行建模的单元的代数和后续实现。由于Scala模块可以组合在一起,所以当您需要在领域服务中使用它们时,您可以将它们捆绑到一个单独的模块中。这里有一个例子:
trait PFRepos extends AccountRepository with CustomerRepository        with BankRepository
在模块中定义了仓储的代数之后,您可以基于用于存储仓储元素的数据库来实现特定的实现。下面可能是基于Redis 键值对存储的实现的概要:

这里写图片描述

INJECTING A REPOSITORY INTO A SERVICE(向服务中注入存储库)

现在,您已经将仓储组织到各种模块中,并实现了多个后端,您需要找到一种方法将这些仓储注入到领域服务中。在我们的领域模型中,服务是用户与之交互的主要粗糙抽象层。为服务提供一个好的API是优秀模型设计的主要标准之一。通过好的API,我指的是简洁、简明、和最重要---可组合。领域服务需要访问仓储的api---你将如何建模这种交互??你可以从最简单的开始----将仓储作为参数传递进去。下面是清单3.1中所讨论的领域服务模块,该模块发布与在客户帐户上操作相关的api,在这里您将仓储作为参数注入到服务方法中(作者注:请注意第3.1节的api代数中的一个不同之处。这里你没有通过一个账户;相反,您通过一个帐户编号,因为您将使用存储库来查找帐户聚合。):

这里写图片描述

这是最简单的方法,但同时也是最幼稚的方法。考虑下面的例子并尝试识别出问题。这是一个使用由AccountService发布的服务的示例客户帐户的计算的示例:

这里写图片描述

问题如下:>Verbosity(冗长)----API的用户需要将仓储作为强制参数传递给该方法。对于服务方法的单个调用来说,这是可以的,但是当您在多个序列调用中组合更大的抽象时(如前面的例子所示),它显然是冗长的和样板式的。>Coupling with the context of the API(与API的上下文耦合)---对于服务方法的每次调用,都要传递仓储的实例,这意味着仓储与API的上下文是紧密耦合的。但在现实中,存储库形成了环境的上下文----它就像一个用于服务方法访问和执行所需操作的存储。>Lack of compositionality(组合性的缺乏)----在这个实现中,您将仓储作为一个值注入。通过在计算的上下文中进行注入,可以使您的API更加的可组合的。一种方法是将参数提升到一个柯里化的形式。这就是你接下来要做的。

WANT COMPOSITIONALITY ? CURRY IT !(想要组合性?柯里化它 !)

所有这三个问题的解决方案是将仓储变成服务方法的一个柯里化的参数。当你通过“for解析”来排序调用时,您可以通过计算来线性化仓储,并延迟注入,直到完成组合函数的最终评估。这样,您就不会在注入部分中丢失任何东西,并在api的组合性上获得很多好处。让我们看一下这一技术在我们的用例中应用.首先,您要更改方法的代数,以将仓储参数提升到一个柯里化形式(请看下面的清单)

这里写图片描述

现在考虑一下这样编写代码时会发生什么情况?

这里写图片描述

PS:没学过Scala的可能不知道Function1什么意思,它是Scala的一个类,基本等同于函数字面量。下面是一段源码,帮助理解
@annotation.implicitNotFound(msg = "No implicit view available from ${T1} => ${R}.")trait Function1[@specialized(scala.Int, scala.Long, scala.Float, scala.Double) -T1, @specialized(scala.Unit, scala.Boolean, scala.Int, scala.Float, scala.Long, scala.Double) +R] extends AnyRef { self =>  /** Apply the body of this function to the argument.   *  @return   the result of function application.   */  def apply(v1: T1): R  /** Composes two instances of Function1 in a new Function1, with this function applied last.   *   *  @tparam   A   the type to which function `g` can be applied   *  @param    g   a function A => T1   *  @return       a new function `f` such that `f(x) == apply(g(x))`   */  @annotation.unspecialized def compose[A](g: A => T1): A => R = { x => apply(g(x)) }  /** Composes two instances of Function1 in a new Function1, with this function applied first.   *   *  @tparam   A   the result type of function `g`   *  @param    g   a function R => A   *  @return       a new function `f` such that `f(x) == g(apply(x))`   */  @annotation.unspecialized def andThen[A](g: R => A): T1 => A = { x => g(apply(x)) }  override def toString() = "<function1>"}
这段代码能正常工作吗?很明显是不可能的!!但是如果你给function1提供了一些额外的功能,它就是那种通过解析而被线程化的类型,那么它就可以运行正常啦。我确信你现在已经意识到所谓的“额外的力量”是Function1上定义的flatMap的力量。这并不是Scala标准库所提供的,但是您可以将Function1作为一个monad并给它一个flatMap。当我们讨论monads时,您将在第四章了解到细节。与此同时,如果您很好奇,在线图书仓储中的第3章的代码有一个用于Function1的monads的实现。假设您在Function1中定义了flatMap,那么您在调用函数时,您早期的计算操作op会返回什么呢?
scala> import App._import App._scala> op("a-123")res0: AccountRepository => scala.util.Try[Balance] = <function1>
请注意,完整的表达式尚未被评估----它只是一个被返回的复合函数。现在您必须将一个仓储显式地传递给返回值来评估整个计算。或者,您可以使用任何其他计算返回 Function1[AccountRepository, _]的计算,并推迟评估,直到您构建了整个计算管道。后一种方法被称为building abstractions through incremental composition(通过增量组合构建抽象),这种方法在函数式编程中被用作一种规范。与将仓储作为参数传递给API的方法相比,这是一个很大的改进。但是如果你想用其他一些不是Function1的计算来组合op呢?它可以是一些其他的效果,如List或Option。我们目前的方法并没有解决这个问题因为你没有必要去复合多种monadic效果。

THE READER MONAD
(译者注:这个以小部分翻译的不完善,也比较拗口,因为一些名词确实没听说过,所以翻译比较机械,当然在后续我会将这一部分再查验一次)

有时,你的函数模型可能需要来自环境的一些额外的输入,而不是作为显式参数传递的输入。并非使函数去访问全局命名空间,Reader(读取器)monad而是使得函数可以访问可以使其获取所需的参数的环境。刚刚讨论过的技术可以很好地完成;您可以使用AccountService的API返回的函数,将额外的信息传递给计算。出于实际的目的,如果您需要一个用于存储库访问的Reader(读取器)monad,您可以使用早前的实现技术。在函数式编程中,复合和构建计算管道并将评估推迟到最后是一种常见的做法。在前面的部分中,计算操作op是一个Function1,它可能需要由一个包含一个List或者一个Option的monadic管道组成。你需要做这种组合的monad转换器。但是Function1没有任何转换器可以用来将你的计算叠加到一个monadic管道中.让我们通过引入另一个间接层来解决这个问题。您将把Function1包装成另一个抽象,您将调用Reader.正如你在第五章中看到的,Reader(读取器)可以有一个转换器,ReaderT,它可以用来将Reader(读取器)与其他的monad复合起来。

这里写图片描述

这里,Reader[R,A]只是一个函数的包装器,该函数在环境R上运行以生成A。在我们的用例中,R是用来读取A的仓储。现在您要做的唯一一件事就是实现map和flatMap,以便在管道中启用顺序读和转换。下面的清单显示了可以与我们的领域服务API相复合的Reader(读取器)。

这里写图片描述

下面的清单显示了服务api,您可以在其中更新代数来反映这种变化。

这里写图片描述

Reader(读取器)就位了,这里就是从用户角度看的服务API的复合。而且您还没有传递任何具体的仓储的实现!客户端代码仍然是相同的,不管您使用的是显式的Reader抽象还是将Function1作为Reader:

这里写图片描述

很好,也很整洁,当你执行这个代码片段时,你会得到一个Reader的实例。作为最后一步,您调用Reader的run函数并传递环境,传递的环境是AccountRepository的一个具体的实现。执行op("a123").run(AccountRepository)将会在存款和取款的顺序结束时,返回给你账户余额。使用前面组合的策略,稍后评估也会使测试变得简单。因为您推迟了存储库的注入,直到评估阶段之前,您可以为单元测试提供一个替代的实现。如果您的仓储基于企业后端数据库,那么您可以将其进行交换,并插入一个简单的内存实现,用于单元测试您的服务。这本书的网页提供了一个完整的AccountService的实现,并具有使用Reader(读取器)monads注入的 AccountRepository。在这种情况下,Reader(读取器)monad产生以下好处:>It decomplects the implementation. >通过减少应用程序代码中的样板文件,它使实现更加友好>它在API的使用站点(而不是定义)中对具体的仓储实现进行了延迟注入,使设计更加模块化。这种技术也称为依赖注入。>将Function1作为Reader,是Reader monad的一种实用性实现.如果你不需要在管道上与monad转换器复合,你可以自由使用它。>因为一个reader将计算从环境中解出,它使得单元测试变得简单。将实现替换掉,并插入一个mock,可以轻松地进行单元测试。PS:练习3.3注入多个依赖项在本节的示例中,通过使用Reader(读取器)monad将一个存储库注入到领域服务中。在许多情况下,您需要向领域服务注入多个依赖项。例如,您可能需要将多个仓储或另一个服务或一些配置参数作为对领域服务的依赖项注入。建议一个适当的策略来处理在AccountService中对多个依赖项的注入。一种方法是将所有依赖项合并到一个单独的环境类型中,然后将其作为单个依赖项传递。本节的主要思想是展示如何将存储库从上下文中插入到服务api中,而不是硬编码具体的依赖项。正如您所看到的,使用Reader(读取器)monad是一个这样的选择。我们讨论了实现Reader(读取器)monad的两种变体。在我们讨论的选项中,您将您的仓储作为环境注入到您的计算中。但是您也可以将依赖项注入更高的粒度级别(例如,在模块中),然后它们也可以在函数级别上可用。这种方法会导致您的总体模型的略有不同的架构,但是现在很多人都使用它。请看一下Macwire(https://github.com/adamw/macwire),它的设计理念是实现这一策略的简单方法.这本书主要使用了Reader(读取器)monad模式的变体,用于在领域服务中注入仓储。

这里写图片描述

PS:上图3.4的英文注释:三种领域对象的生命周期模式的关系图。聚合是链中最重要和最活跃的成员。它们将驻留在内存中,并呈现域模型的大部分功能。仓储和工厂起到了辅助作用。

3.3.7 更有效的使用生命周期模式

我们已经谈了很多关于聚合和工厂的问题,以及如何在一个函数性的世界中对它们进行处理。接下来,我将重点介绍这个强大的设计模式的主要部分,并提供其他实现技术的附加指针。所讨论的三种模式之间的关系的总结如图3.4所示。>聚合是域对象的主要内存表示---实体和值对象。精心设计它们,因为这些数据结构会成为领域api交互的主要点。>抽象地创建像智能构造器这样的工厂的聚合是有两个目的的(a)创建过程的细节从用户抽象出来(b)如果您有相同类的多个子类型,您有时可以在一个API中抽象创建所有这些子类型。>永远不要在原处修改聚合。使用像透镜这样的数据结构,可以进行功能更新,还也可以提供组合api。另一个数据结构也用于操作聚合的嵌套结构,就是zipper。我们不会在这里讨论zippers实现,但你可以从它的创造者Gérard Huet的原始论文中了解到它们的功能(论文地址:http://dl.acm.org/citation.cfm?id=969872),或者可以参考Scalaz或者Shapeless的实现>代数数据类型便于模式匹配,正如您在第2章中看到的。模式匹配也可以用于聚合。不过,最好还是不要采用这种方法,因为它有时会导致无意的实现泄漏。当然Scala提供抽取器来帮你解决这个问题>总是需要关注你的聚合需要遵守的代数法则。在您的测试套件中保持它们作为明确的可验证属性。在本节中,我们还研究了一些通用的函数编程技术和抽象,并将它们应用到我们的领域模型实现中。在这本书中,你将会越来越多地利用这些抽象和其他类似的结构。您必须理解这些抽象是如何工作的。如果你对这个话题仍然不太满意,那就再读一遍,直到你掌握好了。图3.5总结了我们讨论的抽象概念,并将它们映射到我们在领域建模中使用它们的方式。

这里写图片描述

PS:图3.5的英文注释通用函数编程抽象和技术以及其映射到我们在领域建模中使用它们的方式。在我们讨论域建模的其他元素时,您将在本书的后面看到更多的用例。

3.4 总结

您学习了领域建模的基本模式,以及如何使用代数技术来为您的域模型设计api。代数API设计是本书中反复出现的主题之一。以下是这一章的主要内容:>Thinking in algebra(代数思想):您学习了如何根据模块定义的代数来考虑API设计。这与如何在OO系统中设计api不同。最初的焦点是模型领域行为的操作,以及如何将它们收集到模块中。>Type-driven composition(类型驱动复合):您首先识别单个api的代数,然后通过调整类型和使用将它们绑定在一起的计算来组成更大的行为。>Separation of concerns(关注点分离):你应该将解释器(就是实现)与代数分离开来;你看到了一个完整的例子。这是软件工程中关注点分离的又一个例子。>Aggregate as the unit of consistency(聚合作为一致性的单位):使用代数的法则/定律来实现一个集合的一致性边界。>Functional implementation of domain object patterns(领域对象模式的功能实现):通过使用函数式编程的原则,实现一个域对象的生命周期模式,如工厂、存储库和聚合。您了解了如何使用代数数据类型设计聚合,使用透镜、设计存储库更新它们,并使用诸如reader monads之类的功能技术在领域服务中注入仓储。工厂是创造物体的东西---我们讨论了在Scala中实现工厂的智能构造方法。
阅读全文
0 0