面向对象与函数编程的比较

最近Bob大叔发表了OO vs FP博文,文章推崇面向对象与函数编程融合。大意翻译如下:

一个朋友在Facebook发布如下一个对比,这激怒了我,如下图:


有很多程序员说过类似的事情, 他们认为面向对象和函数式编程是互斥形式的编程。 函数编程狂热分子好像是象牙塔的高等生命看不起地上穷且天真的OO程序员。而OO程序员也类似一样指责函数语言中括号污染和浪费。

这些观点都是基于OO与FP的无知。

首先要强调几点:
1. OO不是关于状态

对象不是数据结构,对象可能使用数据结构,但是这些数据结构使用方式是隐藏的,这就是为什么数据字段是私有的原因,从对象外部看,你看不见任何状态,所有你看到的都是函数方法,这样对象也是关于函数方法的,不是关于状态。

当对象用作数据结构时,它是一种设计风格,比如Hibernate自称自己是对象-关系数据库的映射,其实不正确,ORM并不是关系数据库和对象的映射,而是关系数据库和数据结构的映射,这些数据结构并不是对象。

对象是一包函数,不是一包数据。

2.函数编程类似OOP是由操作数据的函数组成。
每个函数编程程序都是由操作数据的一系列的函数组成,每个OO程式也是由一系列操作数据的函数组成。

对于OO程序员将函数和数据定义在一起,事实上,所有程序都是函数绑定到数据。

你也许认为这里绑定的方式还是有区别的,但是仔细想想这是很愚蠢的,难道 f(o), o.f(), 和 (f o)有区别吗?难道区别真的是函数调用语法不同吗?

区别
那么OO与 FP的区别是什么?什么是OO有但是FP不能做的,而FP能做的OO则不能的?

1. FP规定了分配的纪律
真正函数编程并没有分配符号,你不能改变变量的状态,确实,变量这个词语在函数编程中属于用词不当,你其实不能改变它们。

函数编程经常会说函数是第一等公民,smalltalk确实是将函数作为一等公民,但是small talk是面向对象语言,不是函数语言。

函数语言与非函数语言的区别是函数语言并不分配statement。

这意味着在函数语言中从来不能改变状态吗?不是,函数语言通常提供某种方式让你改变状态, F允许你定于“可变变量”,Clojure允许你创建一个特殊对象,其状态可以通过魔术的咒语来改变。

关键是一种函数式语言对变化状态通过强加某种仪式施以纪律。 你必须通过正确的方式去做。

2. OO强加函数指针的约束
一些人认为对象是真实世界的对象映射,这样OOP更加贴近我们的思考方式,其实OO真正特点是使用方便的多态性替代了函数指针。

如何实现多态?使用函数指针实现, OO语言简化这种实现,隐藏了函数指针,因为函数指针难以管理得很好,因此这点无疑是值得肯定的,试图使用函数指针如C编写一个多多态代码看看?它们会有复杂不方便的约定,需要遵循“每种情况下”,这通常不现实的。

在Java中,你调用的每个函数都是多态的,你没有办法调用不是多态的函数,那就意味着每个java函数间接地通过函数指针被调用。

如果你需要在C中使用多态,你得自己管理这些指针,这很难,如果你想在Lisp中使用多态性,你也得自己管理指针,将它们作为参数传送到高阶算法(这是一种策略模式),但是在OO语言中,这些指针语言已经帮助你管理,语言会初始化它们 转换marshal 它们 通过它们调用函数。

OO和FP是相互排斥的?
这两者排斥吗?你能有一种语言既能约束变量再分配和函数指针吗?当然,这两者并不是互相排斥的,你可以编写OO-functional程序。

那是否意味着所有OO程序员的设计原则和设计模式都可以被函数程序员使用呢?如果他们确实接受面向对象在函数指针上的约束他们就会。

但是函数编程者为什么要这样做?有什么好处?他们会得到类似OO程序员约束分配的好处吗?

多态好处
多态只有一个好处,而且很大,能够将源码和运行的依赖进行反转。

大多数系统一个函数调用另外一个函数,运行和源码都会发生相同方向的依赖,调用模块依赖于被调用模块,而多态被注射入于两者,那么就存在一个源码依赖的反转,当然运行时刻调用模块还是依赖被调用模块(源码阶段不是,是通过反转或DI依赖注入将依赖的被调用模块注入到调用模块中),源码阶段的调用模块就再也不依赖被调用的源码了,而两者只依赖一个多态接口。

这种反转允许被调用模块扮演一种类似插件作用,确实,这是也是插件工作原理。

插件架构是健壮的,因为稳定的高价值业务规则能够不再依赖于低价值易变化的模块,比如用户接口和数据库(用户接口是高价值,数据库易于变化)

最终的结果是,为了是健壮的系统在重大架构内必须使用多态性。

不变性的好处
不再重新分配变量值的好处是:如果你从来不更新就不会有并发更新会发生的问题。

因为函数编程不再做变量重新分配,可变性被系统特殊仪式照顾,只是预留给非常需要的时候使用。从多个线程和多核角度看这样做本质上是安全的。

函数编程的底线是多处理和多核环境中更加安全。

深层哲学
当然,面向对象和函数式编程信徒们都将抗议我这种简化分析。 他们会认为,有深厚的哲学区别,以及包括心理和数学原因是他们最喜欢的理由。

我的反应是: 呸!

每个人都认为他们是最好的方式。 每个人都是错误的。

设计原则和设计模式?
前面谈到了有人认为几十年以来设计原则和设计模式仅仅适用OO,而函数编程将它们减少到:函数。

这个想法是极端的疯狂。 不管你的编程风格软件设计的原则仍然适用,。 你已经决定使用一种没有一个赋值运算符的语言并不意味着可以忽略单一责任原则;或自动开闭原则。 策略模式对多态性的利用并不意味着该模式不能用于函数式语言。

底线就在这里。 面向对象编程是好的,只有当你知道它是什么。 函数式编程是好的,也只有当你知道它是什么。 函数风格的OO编程也是很好的,一旦你知道它是什么。


[该贴被banq于2014-11-26 09:02修改过]

虽然FP和OO可以融合,但是不代表它们如同凹凸那样正好互补,它们可能是两个光滑正方形,但是没有凹凸接口可以填补对方。

文中谈到了OO和FP都是函数的组合,但是我个人认为:虽然都是函数组合,但是组合函数的方式不同,OOP是将围绕状态操作的一系列函数组合在一起,而FP则是按照操作流顺序组合,OOP类似围绕一个核心的圆形,而FP类似一个线性。

打个比方 ,OO如同锤子,FP如同扳手,两者还是有各种特点,虽然从它们使用场景来说可以共同解决问题,但是从分解观点来看,两者还是显著不同的两个事物。

感觉函数式是把整个系统看作一个没有子节点的节点,所有的函数都直接挂在这个唯一的节点下。而OO引入了组织结构,OO会在这个系统节点下首先对问题在结构上进行分类,建立出合适规模的子孙节点,然后把函数绑定在子孙节点中去。OO系统中索引函数的方式是沿着组织结构树进行索引,OO中的所有函数是棵树,而函数式系统中的所有函数是个列表。
也就是说OO系统中的每一条函数记录上都会有附加上的组织结构属性,而函数式系统中没有。
OO系统中的每一条函数记录上可能会有一个取值形如“Person.Employee”的命名为"ObjectType"的树形,一条函数记录上还有一个命名为"FunctionCode"取值为"Fire"的属性。在OO系统下标识函数的标识是"Person.Employee.Fire"这样的具有层级结构的值,而在函数式系统中是"ADBE7E3ABC23471A"这个没有添加任何组织结构的值。

我错了,函数式也有组织结构,函数式的组织结构是函数的作用域链,是闭包.
[该贴被anycmd于2014-11-26 14:43修改过]

要理解函数式编程,最好学习一种函数式语言。个人感觉函数式语言采用几乎统一的模式处理所有的问题,没有面向对象那么多的概念和武器。
比如,这是一个获取随机字符串的函数:


(defn random-str [size]
(cstr/join (take size (repeatedly #(rand-nth "0123456789abcdefghijklmnopqrstuvwxyz")))))

我们从最里面的括号开始:
随机的从字符序列(这里就不说字符串了,clojure里面序列几乎无处不在),repeatedly(不停的),take(取size个),然后串接起来。

再比如,java的io,功能强大,但是使用起来非常繁琐。google的guava包这么受欢迎,因为通过抽象把io行为统一化。

如果你使用clojure,也会体会到这种统一化带来的便利。

这个序列处理文件行:


(with-open [rdr (clojure.java.io/reader "/etc/passwd")]
(count (line-seq rdr)))

又是序列,然后count。
(count "abc") => 3
(count [1 2 3]) => 3
(first "abc" => \a

很多人还是不明白语言对于逻辑的重要性,从很多例子可以看出,语言严谨性越低,引发问题的概率越高。不单是编程语言,日常的自然语言也是如此。所谓的规范就是增加语言的逻辑表达能力,很不幸的是不能直接从语言体现的逻辑,往往是异常复杂的。语言越是“简单”,问题就会出现几率就越高。

面向对象增加各种原则,模式就是这种情况。你觉得合理,是你没明白这本来就该是语言本身所具备的,而你只是深信他不具备,当他出现时,你就觉得是这是一个壮举。你不觉得奇怪么?你不觉你很奇怪么?

函数式的生产力,远超所有人所想,知乎上的一片文章精通haskell是一种怎样的体验。看看用了多少天完成一个opengl的库?一个星期不到,一个人。ghc本身就是一个hasekll项目,维护了多少年还充满生机?

PS:深信前人教导,而从不去怀疑。先入为主实在是太可怕了,哪怕你觉得他错了,也懒得去改,让大家一起错下去吧,这样我就不是错了。

"所有程序都是函数绑定到数据",这句话甚有道理!!