基于函数式编程的领域驱动设计 - Scott Wlaschin


Scott Wlaschin 是一名开发人员、架构师和作家。他是流行的 F# 网站 fsharpforfunandprofit.com 的作者,以及 Pragmatic Bookshelf 出版的《Domain Modeling Made Functional》一书的作者。Scott 以其对函数式编程的非学术方法而闻名。
以下是摘要,原文点击标题
 
在一个经典的数学函数中,事物是不会改变的。例如,当你说二加二时,当你做加减乘除时,数字二不会改变。你会有一个新的数字,你不会改变原来的数字,日期也是如此。当你在一个日期上添加一个时间,你不会修改原来的日期。

如果你把这个逻辑应用到所有的事情上,那么什么都不会改变,你总是创造新的东西。
这使得理解程序变得更加容易,因为你总是知道你所做的事情没有被修改,你总是有新的事情要做。

那么,为什么现在使用函数式编程?因为这种不变性,也因为这种组合,没有继承性。
因此,一切都趋向于非常明确:
每一个参数都是明确地传入的。

函数式编程没有正式的定义。我的定义是,它是一种编程风格,你用函数来做一切事情。

在面向对象编程中,基本单元是一个类。而在函数式编程中,基本单元是一个函数。在函数式编程中,如果你想改变某个东西的行为,比如说策略模式,你就传入一个函数。如果你想组成一个更大的函数集,你就把两个函数组合在一起。
基本上,你所做的一切就是函数。你拿着这些函数,把它们连锁在一起,典型地,变成某种管道。你甚至可能有一个函数,输入是一个函数,而输出是一个函数。所以你可以把它叫做函数转换器。

在基于类的编程中,有很多隐含的代码。我是说,你可以有一个没有参数的方法。它返回void,没有参数。在幕后,它正在做一些事情,因为它正在使用实例变量或其他东西。但你不能真正看到它在做什么。而这正是面向对象编程的全部意义所在。
面向对象的整个要点是封装。你应该把它的工作隐藏起来。

函数式编程则正好相反。它的理念是所有的东西都是透明的。所有的东西都是公开的。没有隐藏的数据。

一般来说,当你在编码时,如果所有的东西都是不可变的,那么人们是否能看到一个对象的内部结构其实并不重要,因为他们不能改变它。

不可变性的一个方面是,如果所有的东西都是不可变的,它也能很好地支持并发性。
所以你不会有副作用,你也不必担心多个进程访问同一个实例。

虽然我不得不说这有点夸大其词。我的意思是,那是他们对函数式编程夸大的事情之一,就是它的并发性非常好。它在处理方面做得非常好,它确实如此。但即使它没有,我也不认为那是使用它的主要原因。

主要原因是你用函数式程序编写的程序,我认为它们往往更清晰,更容易理解。

有时使用对象,你往往会得到我所说的对象汤,即一个对象与另一个对象调用,后者与另一个对象调用,后者与第一个对象调用。所有这些对象都在相互调用,真的很难将它们分开。如果你曾经调试过这样的东西,那真的很痛苦。

函数式编程在某种程度上不会这样做,因为它的压力是让事情保持简单。当每一个参数都被传入时,要有一百个参数真的很困难。有一种自然的压力,让事情保持简单,让小的组件粘在一起。(banq注:函数管道彼此链接是一个方向的依赖调用,比对象正反两个方向的调用依赖要简单一半)

 
DDD与FP
领域驱动设计并不依附于任何特定的编程范式。它真的不应该是这样。领域驱动设计的理念是,你关注领域而不是技术。所以,你尝试着关注,使用领域中的词汇。你试着在代码中对领域进行建模,并且你试着不要用各种与人无关的技术东西来污染你的领域。因此,它们与你使用哪种编程范式没有关系。

在面向对象的编程中,很容易有很多额外的复杂性,或者人们所说的仪式,像子类和所有这些东西的额外东西。而且很容易出现有很多东西在里面的代码,而这些东西并不是领域的真正组成部分。在现实世界中,你没有基类。你没有代理类。你没有经理类。你没有接口。现实世界的对象没有这些。
(banq注:面向对象恰恰是对现实的映射,Object是客体对象,周围都是Object,这是一种隐喻修辞手法,理工科读点莎士比亚文科还是很重要:数据模型是一种隐喻修辞手法

在函数式编程中,它往往要简单得多,因为函数式编程没有类。所以这些东西都不会发生。它基本上有数据,并且有对数据的操作。

很多人在做领域建模的时候,都会把注意力放在数据方面的事情上。这样你就自然变成数据库驱动的设计,你从数据库表开始,然后再从那里开始。
(banq注:数据库时代的终结 ; 2012年Robert C. Martin(鲍勃大叔)的NO DB

人们已经意识到,在分析一个真实的系统时,企业所关心的是事件(banq注:领域事件),是行动,是工作流程,是活动。
这些才是真正重要的东西,而数据只是传输东西的一种方式(banq注:用事件替代你的DTO数据结构)。
“活动”你想要建模的,你要建模的是一个“有输入的活动”。

一个函数也正是如此:它是一个输入和输出。
所以,函数对于领域建模来说实际上是非常适合的。
 
在FP中建立战术设计模型

DDD中很多都是非常面向对象的,如实体和聚合体,这些是低层次战术的领域驱动设计。

值对象在DDD中是一个对象,如果你有一个不同的对象,里面有相同的数据,那么它就是相同的东西。
在函数式编程中,那是默认的,
但是在面向对象编程中,这并不是默认的。

在面向对象编程中,每个对象都有一个不同的引用。
因此,如果你想让两个东西相等,你必须覆盖等价方法和获取哈希码方法以及所有这些其他的东西。

在面向对象编程中,实体被认为是可变的对象。
在函数式编程中,它们是不可变的。

我不喜欢有这些行话。我认为这有损于让人们理解这些东西,但你完全可以以不同方式做这些相同的事情。

不过,我应该指出的一点是,这个在领域驱动设计中使用的资源库仓储Repository模式,实际上是在领域驱动设计之外使用的。
在面向对象的编程中,把数据库访问和代码以及业务对象混在一起是非常常见的。你可能有一个对象可以自己保存。
人们几乎从不在函数式编程中使用它。
在函数式编程中,[存储库模式]会被认为是一种反模式。这将被认为是一件非常糟糕的事情。
因为在函数式编程中,我们倾向于把可预测的东西和不可预测的东西分开,把确定性的代码和非确定性的代码分开。

我们所说的纯函数是一个总是给出相同结果的函数。对于相同的输入,它总是给出相同的结果。因此,两个数字相加无论怎么加总是给出相同的结果。
现在,从数据库中读取东西就不是这样了。
如果我再次从数据库中读取,我可能会得到一组完全不同的数据。
(banq注:数据库是可变数据存储,每次从数据库读取类似2+2再做一次,2+2做了两次是8,肯定不是2+2=4做了一次,这里采取比喻有些漏洞,缺失上下文的对比)

在函数式编程中,我们倾向于将任何与数据库、文件系统或网络有关的东西分开。
我们试图把它移到核心业务逻辑之外。
因此,我们把核心业务逻辑放在中间,而任何与外部世界有关的东西都在应用程序的边缘。
(banq注:鲍勃大叔Clean架构,实际是将导致状态变化的副作用与正作用分离:副作用是编程头号敌人!如何剥离它?

核心业务逻辑在中间,数据库在外面,因此,它与经典的N层架构完全相反,在那里你的数据库层在底部。
在函数式编程中,数据库是在外面,而不是在里面。

人们一直在讨论其他架构中的这个问题:有一个端口和适配器的架构、洋葱架构、清洁Clean架构、六边形架构。所有这些东西都是为了尝试把领域逻辑放在中间,而把世界的其他部分放在外面。

有趣的是,在面向对象中,人们必须被鼓励这样做(banq注:人们默认却不是这么做,不是将业务和技术分离开来,而是按照 表现层 ->应用层 ->领域层 ->基础设施层这样N层架构,也是Evans原书提出的,但是不要忘记,Evans更多是领域专家,他只是借鉴当时他写书时那个时代的技术架构)
但在函数式编程中,它自动发生,这是默认的行为。

有一些语言,你可以做一些事情,但它并不真正鼓励你去做正确的事情。所以很容易犯错。

我喜欢这样的语言,你真的被强迫做正确的事情。因此,一种语言是不可改变的,并且不让你做数据库的事情。它迫使你把数据库的东西与核心业务逻辑分开。因此,这就是我喜欢函数语言的原因。它们真的鼓励你以某种方式写代码。
 
 
建模
在领域驱动设计中,有两个边界:
第一个边界是他们所说的有边界的上下文。
有界限的上下文的概念是你把你的程序分成子系统。
但每个子系统只能做好一件事,这个子系统中的一切都使用相同的术语,以相同的方式工作。

这听起来非常明显,但写起来却不是非常容易。
我见过很多大的系统,所有的东西都纠缠在一起,你有一些代码的碎片,它们都在一个大的混乱中互动,大的泥球。
(banq注:RefactorFirst:寻找Java代码库中无所不包的大型“上帝”类

在一个有边界限制的环境上下文中,你需要保证某些约束或完整性得到满足。
你可以用一个函数来强制执行:如果你有一个输入和输出,你就可以说,好吧,你改变这个东西的唯一方法就是通过这个函数。

而如果在可变的对象中非常常见的做法是:因为你可以改变任何字段。变更一个字段,然后忘记变更其他字段,这是很容易的。而这可能是错误的。如果你有不可变的数据,你就不会有这个问题。如果你不能改变数值,那么一旦它是正确的,那么它就永远不会改变。

实际上,它最终会减少很多防御性编程。如果你有不可变的数据,你通常会在程序的最开始,在程序的边缘验证一些东西。然后一旦你进入了你的程序,你基本上就相信它是有效的,因为它不可能被改变。
 
其他还有F的介绍,更多点击标题见原文