函数式编程的‘元’很重要。(一元函数——函数只有一个参数。任意元函数——函数有任意个参数)
函数式编程 - 图1
函数式编程的特点

  • 纯函数
  • 一级函数
  • 数据不可变
  • 无副作用
  • 无状态
  • 惰性求值

世界从来都是不是一个维度的。有的人推崇 FP,有的人则是 OOP 的死忠粉。这两种编程思想都很好,没有优劣之分。

我本身是从 OOP 开始接触编程的,最近的一些项目中,对 FP 有一些新的认识,下面是我总结的对比。

首先看一看维基百科的函数式编程(functional programming,简称 FP)的概念:

In computer science, functional programming is aprogramming paradigm — a style of building the structure and elements of computer programs — that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.

看了以上的定义,我对 FP 函数式编程的理解主要有两点:

  • 不改变 input
  • 没有 side effect

和面向对象编程(object-oriented programming,简称 OOP)最大的区别就在于:OOP 里子类会继承、改变父类的状态,并且很多时候 method 不是 pure function,会有很多 side effect 产生。

先来看一个(来自网络的)财务软件的例子。这里是在这个例子基础上,我理解的 FP 和 OOP 的对比。

OOP 举例

先来看看用 OOP 是怎么解决此类问题的。

  1. // 这是初始版本
  2. public class IncomeTaxCalculator{
  3. protected double _threshold = 3500;
  4. public double calculate(IncomeRecord record){
  5. double tax = record.salary <= _threshold ? 0 : (record.salary - _threshold) * 0.2;
  6. return tax;
  7. }
  8. }
  9. // 往往 Value Object 一旦发布基本上就很难改变,因为外部已经有很多引用
  10. class IncomeRecord{
  11. String id; // 身份证号
  12. String name; // 姓名
  13. double salary; // 工资
  14. }
  15. // 当需求改变时 OOP 的处理方法
  16. public class IncomeTaxCalculatorV2018 extends IncomeTaxCalculator{
  17. // 2018年9月1号后起征点调整到了 5000,重写 calculate method 加上这个逻辑
  18. public double calculate(IncomeRecord record){
  19. if(today() > date(2018, 9, 1)){
  20. double _threshold = 5000;
  21. }
  22. return super.calculate(record);
  23. }
  24. }
  25. IncomeTaxCalculator calculator = new IncomeTaxCalculator();
  26. calculator.calculate(new IncomeRecord(1234, 'tiger', 10000));
  27. // 需求改变后,只需要使用新的 class 即可:
  28. IncomeTaxCalculator calculator2018 = new IncomeTaxCalculatorV2018();
  29. calculator2018.calculate(new IncomeRecord(1234, 'tiger', 10000));

不可否认,OOP 对可维护性有非常好的支持,把可维护性带到了一个新的高度。但也有一些弊端。

从以上例子可以看出来原来的 class 完全不需要任何改动,有任何的新需求只需要新增一个 subclass 继承原来的 IncomeTaxCalculator 即可。但这样有几个问题:

  1. subclass IncomeTaxCalculatorV2018.calculate() 包含了 today(),即 side effect,如果不这么做,那就需要改变 IncomeRecord,即 input
  2. parent class 内部变量 _threshold 发生了改变

FP 举例

  1. // 初始方法
  2. function calculator(record){
  3. const threshold = 3500;
  4. return record.salary <= threshold ? 0 : (record.salary - _threshold) * 0.2;
  5. }
  6. // 应对需求,新增的计算方法
  7. function calculatorV2018(record){
  8. const threshold = 5000;
  9. return record.salary <= threshold ? 0 : (record.salary - _threshold) * 0.2;
  10. }
  11. // 高阶函数 higher-order function,包装之前的函数
  12. function getCalculator(oldFn, newFn, today){
  13. if(today() > date(2018, 9, 1)){
  14. return newFn;
  15. }else{
  16. return oldFn;
  17. }
  18. }
  19. calculator(new IncomeRecord(1234, 'tiger', 10000));
  20. // 需求改变后,用高阶函数包装之前的函数
  21. const taxCalculatorV2018 = getCalculator(calculator, calculatorV2018, new Date(2018, 9, 1));
  22. taxCalculatorV2018(new IncomeRecord(1234, 'tiger', 10000));

当需求改变后,只需要新增加一个 pure function calculatorV2018()
和一个 higher-order function getCalculator() 来包装之前已经存在的函数
calculator(),使得这些函数都可以变为 pure function。

我认为 FP 最大的优势在于每个函数的 input 和 output 映射,而且期间没有任何 side effect,所以单个函数构造简单,非常容易测试。

但缺点也显而易见,为了应对日益变更的需求,有可能需要不断的有 higher-order function 来包装之前已经存在的 pure function,多层包装之后大大增加了代码复杂度从而将代码变得难以维护和可读性差。(多次抽象包装之下,不易阅读与维护)

一个经典的例子就是 React 的 higher-irder function components,经常在封装了多层之后,已经弄不清楚哪些变量是属于个 component,以及基本的父子关系等等。

总结

FP 和 OOP 都是前辈们探索出来为更好的维护和协同工作而人为发明的 concept,没有谁好谁坏之分。遇到不同的使用场景,选择最合适的即可。