函数式编程与面向对象编程

编程的本质

当写过许许多多程序后,接触了那么多编程模式、设计模式、框架、语言、算法、数据结构以后,就会发现编程的本质万变不离其宗就是,操纵一坨数据。当然操纵的方式有许多,存储的方式也五花八门,但是本质不变,就是访问数据(读取以及改变)。

下面谈一谈我对两种编程方式的理解,就是面向对象编程,以及函数式编程。我用 JavaScript 这种神奇的语言来解释,因为 JavaScript 既可以面向对象编程,也可以函数式编程。

数据存放方式

1、对于 OO(面向对象,下同),数据存放在对象的属性(成员变量)里面,以及静态成员(全局变量)

2、对于函数式,数据存放在闭包(各级作用域)里面,作用域包括全局作用域。

数据访问方式

数据存放方式决定了访问的方式。

1、对于 OO 来说,访问数据(全局变量除外)需要先获取对象的引用,然后再进行操作(直接访问——公共属性,或者调用成员函数/方法访问——私有属性)

2、对于函数式,访问数据是直接访问(通过函数入参或者作用域链查找)

下面上一段代码

1
2
3
4
5
6
7
8
//OO
class Foo {
constructor(){
this.bar = 0
}
}
let foo = new Foo()
foo.bar ++

//函数式

1
2
3
4
5
let bar = 0
function foo(){
bar ++
}
foo()

这是随便写了一些没有用代码,只是为了展示两种风格的编程方式。

从中我们可以看出一些东西,OO 是通过持有,以及传递对象的方式去让别的对象来操作数据,而对象也会是其他对象的成员,层层嵌套。当你想要访问某一个数据的时候,就需要顺着对象的引用链条去找,一步步去操作。

函数式传递的则是函数,调用函数即操作数据,传递函数的时候其实隐含着传递了函数创建的时候所附带的作用域,这个在表面上看不出来,在底层是有的。

OO 所谓的对象,本质上就是函数式中的作用域。

为了理解这句话,我们来看高阶函数的本质

1
2
3
4
5
6
7
8
function foo(x){
let bar = x
return function(){
return bar
}
}
let Bar = foo(1)
console.log(Bar())//1

当调用 foo(1)的时候返回一个函数,这个函数可以访问 foo 函数内部的 bar 变量,这就是高阶函数。如果翻译成 OO 思想大家就知道怎么回事了。

1
2
3
4
5
6
7
8
9
10
class Foo{
constructor(x){
this.bar = x
}
Bar(){
return this.bar
}
}
let foo = new Foo(1)
console.log(foo.Bar())

看上去是不是差不多。其实在 C#中对 lambda 表达式的编译就是这个原理。会内部创建一些类。

当然高阶函数的写法肯定不会这么啰嗦,会写的比较优雅。(这就是本质区别)

1
2
3
let foo = bar=>()=>bar
let Bar = foo(1)
console.log(Bar())//1

所以 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
2
               ↓<过程式>           ↓<面向对象>       ↓<函数式>
[命令式风格] o----------------------------------------------------> [声明式风格]

箭头从左到右抽象程度逐渐上升。过程式风格有函数的概念,和纯粹的指令相比可以用简短的函数替代一段指令;面向对象风格包装了一层 class 这样的概念,允许打包的一组数据自己持有一些信息,使用者调用方法前不需要知道所有的事情;函数式风格允许把“处理过程”本身当作参数,传入的处理过程的参数就代表具体操作时遇到的实体。
函数式编程就是用函数的组合来解决问题,面向对象是建很多对象来互相交互解决问题,面向过程就是下一步下一步下一步。

例子

输入十个整数,输出大于平均值的数,然后输出排序后的十个数面向过程/C/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int input[10];
int sum = 0;
int tmp;
double average;
for (int i = 0; i < 10; i++)
{
scanf("%d", &input[i]);
sum += input[i];
}

average = sum / 10.0;
for (int i = 0; i < 10; i++)
{
if (input[i] > average)
{
printf("%d\n", input[i]);
}

}
/_Bubble Sort_/
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9 - i; j++)
{
if (input[j] > input[j + 1])
{
tmp = input[j];
input[j] = input[j + 1];
input[j + 1] = tmp;
}
}
}

for (int i = 0; i < 10; i++)
{
printf("%d\n", input[i]);
}

面向对象//C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class solve
{
public:
solve() noexcept;
void sort();
void show();//输出大于均值的数
void print();//打印数组
private:
int input[10];
int sum;
double average;
};
//具体实现省略
int main()
{
auto test = new solve();
test->show();
test->sort();
test->print();
return 0;
}

函数式编程#Python

1
2
3
4
array = [int(input()) for i in range(10)]
average = sum(array) / 10;
print(list(filter(lambda x: x > average, array)))
print(sorted(array))