函数式编程与面向对象编程
编程的本质
当写过许许多多程序后,接触了那么多编程模式、设计模式、框架、语言、算法、数据结构以后,就会发现编程的本质万变不离其宗就是,操纵一坨数据。当然操纵的方式有许多,存储的方式也五花八门,但是本质不变,就是访问数据(读取以及改变)。
下面谈一谈我对两种编程方式的理解,就是面向对象编程,以及函数式编程。我用 JavaScript 这种神奇的语言来解释,因为 JavaScript 既可以面向对象编程,也可以函数式编程。
数据存放方式
1、对于 OO(面向对象,下同),数据存放在对象的属性(成员变量)里面,以及静态成员(全局变量)
2、对于函数式,数据存放在闭包(各级作用域)里面,作用域包括全局作用域。
数据访问方式
数据存放方式决定了访问的方式。
1、对于 OO 来说,访问数据(全局变量除外)需要先获取对象的引用,然后再进行操作(直接访问——公共属性,或者调用成员函数/方法访问——私有属性)
2、对于函数式,访问数据是直接访问(通过函数入参或者作用域链查找)
下面上一段代码
1 | //OO |
//函数式
1 | let bar = 0 |
这是随便写了一些没有用代码,只是为了展示两种风格的编程方式。
从中我们可以看出一些东西,OO 是通过持有,以及传递对象的方式去让别的对象来操作数据,而对象也会是其他对象的成员,层层嵌套。当你想要访问某一个数据的时候,就需要顺着对象的引用链条去找,一步步去操作。
函数式传递的则是函数,调用函数即操作数据,传递函数的时候其实隐含着传递了函数创建的时候所附带的作用域,这个在表面上看不出来,在底层是有的。
OO 所谓的对象,本质上就是函数式中的作用域。
为了理解这句话,我们来看高阶函数的本质
1 | function foo(x){ |
当调用 foo(1)的时候返回一个函数,这个函数可以访问 foo 函数内部的 bar 变量,这就是高阶函数。如果翻译成 OO 思想大家就知道怎么回事了。
1 | class Foo{ |
看上去是不是差不多。其实在 C#中对 lambda 表达式的编译就是这个原理。会内部创建一些类。
当然高阶函数的写法肯定不会这么啰嗦,会写的比较优雅。(这就是本质区别)
1 | let foo = bar=>()=>bar |
所以 OO 编程,是面向作用域编程,而函数式编程,是面向功能编程。
函数式编程有一个巨大的优势,就是作用域链,也就是说函数可以访问的变量范围要远远大于 OO,灵活性就成几何级数上升。换句话也可以说 OO 的约束性更强。
FP 适合数据处理,OOP 适合外包业务领域
函数式编程(FP)和面向对象编程(OOP)直接相比是一种常见的误解。这种比较应该是 FP 和 OOP 支持者之间互相怼的产物。
事实上,FP 和 OOP 是两种不同的看待事物的方式。FP 强调“everything is lambda”,并且强调在逻辑处理中不变性的重要性。不变到什么地步呢?原教旨主义的 FP 就连普通的循环都不可以写(因为循环都有个变化的 idx 或者条件之类的变量),必须用递归实现。这样做的结果就是把一切“状态”都消除。任何“状态”都是由确定的输入经过确定的一组函数处理得到的最终结果。 FP 适合的领域是处理数据。
OOP 强调“everything is object”,以及 object 之间的消息传递。通过消息传递改变每个 Object 的内部状态。OOP 之父 Alan Kay 表示”OOP is all about messaging”。利用 OOP 建模,都会通过某种消息机制来模拟一些场景的处理。比如交易=下单 Object,支付 Object,积分 Object 等之间进行交互当然,实际的 OOP 的程序运行时为了效率一般会用方法调用,而不是真的传递一个物理消息。OOP 适合的领域是企业 ERP,外包业务等。
如果你看懂了上面两个概念,就会发现他们说的事情压根就不在一个频道里。因此各自的好处也不能证明另一方有缺点。
现在经常看到文章表达 FP 如何如何优于 OOP,大概原因有这么两点:第一点是,OOP 早期不切实际的吹牛皮,吹爆了。很多人谈起 OOP,都会有“用了 OOP,代码耦合就小了,就容易维护了,扩展就方便了,代码就更容易复用了等等“的第一印象。但实际上这并不一定发生。软件设计并非因为 OOP 就直接自动变好了。因此很多程序员在趟坑多年后可能会感觉“我擦,学了这么多年,全是假的“。更进一步的,像 Java 这样的“纯 OOP”语言迫使程序员并不需要 OOP 的情况下也得照着 OOP 的方式去写代码,结果啰嗦又臃肿。所以很多人越来越讨厌 OOP 其实是可以理解的。(比如这篇 Goodbye, Objected Oriented Programming)。现在的 Java 程序大量使用反射、lambda 等技术,已经不是早期那个单纯 OOP 语言了。第二点是现代程序开始往并发发展。而 FP 的不可变,没有副作用等特性恰好让并发编程变得不容易出错。并且配合多种并发模型(如 CSP、Map Reduce、Fork & Join、Promise 等),可以解决很多高并发的问题,显得高、大、上、酷。
但是,我非常赞同《人月神话》的著名论断——没有银弹。不论 OOP 还是 FP,用好了都可以发挥作用,用不好一样吃瘪。
举几个例子,一个业务领域建模,其实模拟的就是现实当中的不同角色的人/机构的工作方式。因为如果是人/机构互相协作,就是通过消息来协作的。比如博士生想发文章,先得自己写,写了老板审阅,完事发给期刊编辑,编辑找同行评议,完事发表,发表的结果会收录到某个文献索引数据库。这个过程就是多个独立的“对象”在相互协作的结果。因此 OOP 在这个层面上对这个流程进行抽象是很合适的。当然你也可以说,这时我用 FP 的各种动作函数的组织来描述这个过程,也是可以的。但是如果比较一下,这个场景用 FP 和 OOP 建模,哪个更容易理解呢?
再比如,对一组数据做加工,先查询,然后聚合,聚合后排序,再 join,再排序,再聚合,再转换(map)得到最终的结果。这个过程,用 FP 的函数就很自然,因为这一看就是 result = func1(func2(func3…funcN(x))))这时用 OOP 呢?给每一个步骤建一个 class?然后把排序、聚合等操作放在 class 里?抽象个基类?或者弄个 XXXUtils 的静态方法集合类?当然都可以做,但是很明显这不是个好的设计。再再比如,一个业务流程,就是一组步骤:第一步如何如何,第二部如何如何……。这时用 FP 和 OOP 都不能很好的表达问题(可能 FP 接近点)。这其实是典型的“指令式编程“。如果业务逻辑如此,那么就照着一步一步做就是最好的,而不是抽取函数和不变状态;或者定义一些根本无意义的 class。
说了这么多,其实希望表达的意思是:到底用哪种编程模式,要看问题本身适合哪个。哪个用起来自然,和问题本身特质搭配,那就用哪个。用对了,事半功倍;用错了,就各种纠结拧巴。你希望你一个东西模拟为 Object,前提是这个东西本身容易抽象成一个 Object;你希望你一个数据可以抽象为一组函数执行的组合,前提是这样理解更自然,更舒服。此外,同一个问题可以拆解为不同的层次,不同的层次可以使用各自适合的方式。比如高层的可以 OOP,具体到某个执行逻辑里可以用 FP 或者指令编程。
对比面向过程,面向对象,函数式编程
函数式编程、面向对象编程和面向过程编程都是思维方式,具体语言的语法不构成限制。这三者可以当作是一条光谱上不同的颜色,一头是命令式风格,一头是声明式风格。
1 | ↓<过程式> ↓<面向对象> ↓<函数式> |
箭头从左到右抽象程度逐渐上升。过程式风格有函数的概念,和纯粹的指令相比可以用简短的函数替代一段指令;面向对象风格包装了一层 class 这样的概念,允许打包的一组数据自己持有一些信息,使用者调用方法前不需要知道所有的事情;函数式风格允许把“处理过程”本身当作参数,传入的处理过程的参数就代表具体操作时遇到的实体。
函数式编程就是用函数的组合来解决问题,面向对象是建很多对象来互相交互解决问题,面向过程就是下一步下一步下一步。
例子
输入十个整数,输出大于平均值的数,然后输出排序后的十个数面向过程/C/
1 | int input[10]; |
面向对象//C++
1 | class solve |
函数式编程#Python
1 | array = [int(input()) for i in range(10)] |