从建造者模式到闭包和作用域
这是「从函数式角度看设计模式」的第六篇。
之所以写这一篇,也是因为在最近几年乃至最近的项目实践里,感觉到了一些有趣的东西呢,这时候回想到之前学校里写项目的时候,会有人「去学习和使用Builder pattern来解决问题」。
那么故事先从Kotlin里面的一个神秘的类型开始:
1 |
|
在我的上古项目里面,有一个这样的函数,里面传入的参数是这样的A.(B) → C
的类型结构。写下这个代码的时候,对Kotlin还算是一知半解,虽然连蒙带猜地用它写出了功能,但对它背后的原理算是不太清楚。
后来,知道了这个特殊的语法,在Kotlin里面,叫做「Receiver type」:
Function types can optionally have an additional receiver type, which is specified before the dot in the notation: the type
A.(B) -> C
represents functions that can be called on a receiver objectA
with a parameterB
and return a valueC
. Function literals with receiver are often used along with these types.
Kotlin官方也有专门的页面讲解这个特殊类型的典型应用——Type safe builder,例如这样的:
1 |
|
看起来跟作为经典设计模式的Builder模式密切相关。那么,Builder模式又是什么呢。
按照定义,「生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象」。
「该模式会将对象构造过程划分为一组步骤, 比如 buildWalls
创建墙壁和 buildDoor
创建房门创建房门等。 每次创建对象时, 你都需要通过生成器对象执行一系列步骤。 重点在于你无需调用所有步骤,而只需调用创建特定对象配置所需的那些步骤即可。」
在Java里面的实现大概长这样。
1 |
|
但是仔细看的话,这个builder有好几个问题。首先,它高度依赖可变状态,本质上,可以理解为就是一个容器,里面有不同的字段,每个build或者set函数说白了就是修改这些字段然后返回这个容器自身来表达「链式调用」。
且不说高度依赖副作用的做法和lambda calculus的许多基本理念的背道而驰,这个做法似乎也没提供什么表达力。Kotlin里面的这个html的例子虽然看起来很简单,但简单扩展一下,就可以引入ADT和一些类型上的操作,结合函数组合的概念,让builder变得类型安全。
但是反过来说,builder模式就难以为力了吧?因为每个函数的返回类型都是Builder容器本身,等于说没有任何标签可以标记「这次构建是否正确」,最后只能退化为运行时,在拿到builder产物的时候才抛出错误。
换句话说,builder本身混淆了作用域和第0函数,而且也因此失去了类型安全。我们知道在Kotlin或Scala里面,type-safe的HTML每次构建后大概会产生什么样的AST,但是,对于一个Java代码,new CarBuilder().setSeats(…).setEngine(…)
,实在没有什么能够让人安全地去知道它长啥样,是否被正确构建的机制。
再进一步说,如果我调用new CarBuilder().setSeats(…).setSeats(…)
两次,或者,如果我需要先setEngine,因为我的seats依赖于engine的产物呢?
在纯函数式的角度,ADT的组合可以让我们相对简单地拆分builder的每个阶段,并且保证整个管线大致正确。显然,Java里面,要么,我们得写上极度臃肿的代码,要么,就完全失去了这种安全保障。有关注这个系列之前的文章的话,你应该也能猜到在纯函数式视角,大概会写出什么样的伪代码了。
进一步地说,还可以使用GADT让方法参数也能在编译时做到类型安全。
1 |
|
说到底,就是确定性和可预测性的问题了。这大概也是Kotlin强调类型安全的地方。
不过话说过来,这个type safe其实也并没有很特殊。因为从FP的角度讲,这就是lambda、闭包、作用域自然而然就引入的内容。
也就是构建DSL的能力。
说起来,提到GADT和DSL,之前也有在讨论visitor pattern的时候提到呢。