函数式设计导读系列(三)

前言

Neal Ford 在《函数式编程思想》(Functional Thinking)中提到面向对象编程是通过封装可变因素控制复杂性(makes code understandable),而函数式编程是通过消除可变因素控制复杂性的。

封装和消除,哪种好理解?

封装是为了构造抽象屏障(Abstract Barrier),到达隐藏信息的目的。任何编程范式都不会缺少封装,因为这是人类简化理解事物的方式。只不过在面向对象编程语言中,封装、继承和多态(polymorphism)被拔到了一种必须充分理解的高度,而 Bob 在函数式设计中挑明了态度,他认为多态比其他两者高出一筹。原因在于进行高层次策略设计时,多态是调整依赖关系行之有效的方法。我认为他说得有一定道理,但是弱化封装这个性质倒和他的中心思想相背离,因为在这本书里,面向对象的封装和函数式的消除是棋逢对手的关键性质,费点笔墨解释清楚,很有必要。

Bob 大叔在第五章里列举了两段伪代码,如下。

image.png{:height 442, :width 780}

image.png

如果你看过我之前写的文章,不难理解这里发生了什么,无非用尾递归消除了 x 的赋值。在 Bob 大叔的眼中,赋值等同于可变性。所以,函数式设计的精髓就是消除可变性。

可是,除了赋值,第一段程序跟封装有什么关系呢?别急,我们稍微重构一下它。

refactor-f.png

refactor-getInput.png

refactor-getInput-f.png

Input x 是一个对象,所以我们就把所有依赖其本身的行为通通封装进它的体内,希望你能感受到封装的力量。这里面有一点洞察,面向对象尽量会把行为放进对象体内,以符合数据内聚性的需要。也因此,对象的状态会原地改变,相当于赋值。

函数式则不然,它的状态无法原地改变,也就意味着它必须以递归或管道的方式自旋或流动起来,那些状态就保持在不可变的外部数据结构当中。

所以 Bob 大叔下了个结论:在状态可变的语言中,行为流过了对象;而在函数式语言中,对象流过行为。

行为流过对象,指的是行为在对象中迭代发生;而对象流过行为则是说行为在对象外顺序发生。或者说面向对象中的赋值(状态变化)在对象里发生,而函数式中的状态变化在对象外发生。所以从做事的横向流程上看,函数式更加清爽明了。而纵向结构上,面向对象则更加边界清晰,粒度合适。究其原因,函数式是一种“以终为始”的思考方式,其定义的每个阶段的产物都是比较完整的;而面向对象则是子问题划分,在子问题中寻求差异化解决方案。

函数式编程的过程与结构

函数式编程是一种管道式的编程风格,Bob 大叔说它更像是铺设和修改数据流的管道,管道中的每道工序更像是包含尾递归的状态转移。

image.png

如果转化成 Clojure 代码,就像是用上了 Thread 宏的管道风格。

image.png

数据起源于物理世界,从一头流向另一头,最后汇入到有副作用的物理世界。

控制住复杂性?

没有那么简单。回到最初的原则,编程范式是对编程方式的规范和约束,而方法的孰优孰劣取决于问题的规模和复杂性,也取决于方法固有的复杂性。遇到非状态机(随时序变化)的问题时,两种方法差异不大;遇到状态机的问题,就要根据规模和复杂度琢磨一下。后续,我们在深入对比分析。

大家好,我是鄢倩,我是 Bob 大叔的《架构整洁之道》中文版的技术审校,也是他的扛鼎之作《敏捷软件开发:原则、模式和实践》重制版的译者。当然,再次有幸参与了大叔的新作《函数式设计》的技术审校,我非常高兴能在这里和大家分享我在审校过程中所学所想。

111724213567_.pic.jpg

FD 发布会.jpeg