想象你回到了你的学校生活,并尝试去学习从数学角度定义一个function(函数)。因为我们在这里讨论函数式编程,这个函数和你在数学课上学到的有哪些不同点呢??
在数学中,函数是一组输入和一组允许的输出之间的关系,每个输入都与一个输出相关。 -----Wikipedia, http://en.wikipedia.org/wiki/Function\_\(mathematics\)
这个定义从来没有提到过对共享可变状态函数的依赖。函数的输出是纯由函数的输入所决定的----特别像图1.5,它将一个函数(f)建模为一个将输入(x)转换为输出(y)的黑盒。在函数编程中,你要努力使你的函数变现的只是像一个数学函数。
问:清单1.4或清单1.5的哪个模型更接近于本节中引入的函数的定义?
既然您已经了解了函数的定义,并理解了如何在领域模型中实现相同的效果,那么这个问题应该是一件很容易的事。对外部可变状态的依赖越小,模型就越接近数学函数的纯洁性。
答:当然是清单1.5。它将Account变为一个不可变的抽象,接近于函数的定义。
在图1.5中,我们的y = f(x)模型,假定函数是平方操作,那么sqyare(3) = 9,不管你运行函数f多数次,都会得带完全相同的结果。那么接下来让我们来分析我们之间介绍的两种Account。
首先呢,在清单1.4的可变版本的Account中,在一个Account对象上执行debit(100)操作的话,产生的结果不仅依赖于我们传入方法的参数100和Account对象自身(也可以考虑一个隐式参数),而且还与其他客户端共享同一对象。这是因为共享Account对象的所有客户端都具有对可变状态的平等访问权。这远非我们在本节所讨论的纯函数。
在图1.5中的不可变版本的Account中,Account对象也持有当前的状态。因此,在Account上执行debit(100)操作
因此,在拥有2000当前余额(balance)的Account对象上调用debit(100),将总是产生一个新的Account对象,其余额(balance)为1900。输出只取决于所提供的输入。这个模型具有数学函数的纯洁性。
好了,现在是揭晓神谕的时候了。不可变的Account模型是您将很快看到的函数模型的面向对象版本。它仍然具有建模为类方法的函数。通过这种方式,你经常会遇到这样的困境:哪个函数应该是哪个类的一部分。此外,编写函数实现作为不同类的方法也会变得困难。
在我们的样例中,debit和credit都是在一个单独账户上的操作,并且您将其作为Account的行为。但是想transfer这样的操作,是需要有两个账户的。那么它是应该作为Account的一部分,还是应该作为一个领域服务呢?在一个账户上你应该如何处理其他服务,比如说每日余额明细表或者利息计算?您可能倾向于将它们放在一个类中,使其成为一个臃肿的抽象。将这些行为放在一个特定的聚合中也会妨碍模块化和组合性。以下是设计功能领域模型时需要遵循的一般原则:
以algebraic data type (代数数据类型)(ADT)建模不可变的状态
模型行为作为模块中的功能,模块表示业务功能的一个粗糙单元(例如,领域服务)。这样,就可以将状态与行为分开。行为比状态更好组合;因此,在模块中保持相关的行为可以实现更多的组合性。
请记住,模块中的行为对ADTs所代表的类型进行操作。
问1.3:回答对与错:面向对象的范式对状态和行为是一对的关系,而函数式编程将状态和行为分离。
让我们首先看一下使用函数式Scala实现的Account模型。然后你就能回答这个问题了。清单1.6是改进早期实现的模型。它包含了相当多的Scala结构,其中一些是您暂时可以忽略的。下面是我们以函数思想建模我们的模型时候的主要关注点:
Scala样本类建模一个ADT。默认情况下,ADT的所有参数都是不可变的,这意味着您不需要任何特殊的机制来确保模型的不可变性。
定义的ADT并不包含任何的行为。注意现在debit和credit都包含在AccountService中,它是你定义的一个领域服务。服务以模块来定义,在这里是以Scala的特质来实现的。Trait(特质)就像混合一样,可以简单将小的模块组合成大的模块。当你需要创建一个模块的实例(即我们上下文的一个服务),你可以使用object关键字。我们以前就提及过,有了函数思想,你就可以将状态与行为分离----现在状态驻留在ADT中,而行为则建模为单独的函数,驻留在模块内。
debit和credit都是纯的函数,因为他们没有与任何特别的对象相关联。相反,它们接受参数,执行一些功能,生成特定的输出,就像图1.5中的y=f(x)模型一样。
清单1.6使用了一些其他的结构,比如 Try,Success,和Failure,这些结构比抛出异常更具有功能性和组合性。即将到来的侧栏“Exceptions in Scala(我会以PS的方方式给出来)”给出了在Scala中处理异常的概述。后面的章节还将讨论这个主题,因为它们详细描述了函数式编程模式。
图1.6总结了使用Scala将面向对象、不可变域模型转换为函数变体的变化。
PS:图1.6的英文翻译
从面向对象的不可变建模到函数抽象。注意,我们已经将状态与行为分隔开了。状态被编码在一个代数数据类型,Account,而行为则属于领域服务。另外,像Try这样的结构,会磅为主我们构建可组合的抽象。
答1.3:主流的面向对象语言鼓励函数被封装到与状态相同的抽象中。在面向类的OO语言中,这种抽象是“类”。
下一节将讨论函数组合。但是让我先来看看另一个很酷的组合效应,它是由你的重构成函数抽象(比如说Try)所带来的。你现在可以组成多个debits(借方)和credits(贷方),如下:
val a = Account("a1", "John", today)
for {
b <- credit(a, 1000)
c <- debit(b, 200)
d <- debit(c, 190)
} yield d
res5: scala.util.Try[Account] = Success(Account(a1,John,Sat Nov 22
02:38:03 GMT+05:30 2014,Balance(610)))
PS:Exceptions in Scala
在函数式编程中,异常被认为是不纯的。为了在功能上处理异常,Scala定义了一个抽象,util.Try,他有两个具体的实现,分别是Success和Failutre。清单1.6使用了这个抽象来处理任何可能会从 generateAuditLog操作产生的异常。请注意,generateAuditLog是一个函数,它需要一个account和一个amount参数,并尝试生成字符串形式的审计日志。以Try[String]作为返回值类型去发布事实数据,操作可以失败,一旦失败了就会返回Failure.现在了解细节并不重要。但是请注意,Try是一种可组合的抽象,并且可以以一种纯粹的、函数式的方式与其他抽象结合起来。