1

前面一段时间对 FP 和 OOP 两者的关系感到比较困惑
我使用的动态语言揉合太多范式, 在这一点上很难做出明确透彻的区分
不过经过这段时间琢磨相对之前感觉要好一些了, 有了一些自己的想法
后面自己的部分会有不少没有验证的地方, 所以应该当成感想来看
需要说明, 我的经验来自动态语言(JavaScript), 相对静态语言会有很多纰漏

网上的解释

当然这种事情先 Google 是必需的, 我当时微博标记了两份比较在意的说明:

我摘录的原文部分应该很有建设性, 就不翻译了:

When you anticipate a different kind of software evolution:

  • Object-oriented languages are good when you have a fixed set of operations on things, and as your code evolves, you primarily add new things. This can be accomplished by adding new classes which implement existing methods, and the existing classes are left alone.
  • Functional languages are good when you have a fixed set of things, and as your code evolves, you primarily add new operations on existing things. This can be accomplished by adding new functions which compute with existing data types, and the existing functions are left alone.

When evolution goes the wrong way, you have problems:

  • Adding a new operation to an object-oriented program may require editing many class definitions to add a new method.
  • Adding a new kind of thing to a functional program may require editing many function definitions to add a new case.

根据估计的未来代码中是数据或者是操作为主, 会有一定的区分
面向对象适合在新系统中接入新的数据, 函数式适合在原有代码上增加新的方法

我觉得上边说的 functional 和纯函数的编程可能还有差别..
另一份实在 Haskell 的 Wiki 上,

这里摘录的是在文章当中的一个引用:

To quote:

"The functional approach to programming is to ask "how is data constructed?". This leads to a style of programming where the data constructors are considered primitive, and where data-consuming code accordingly is defined by pattern-matching over these constructors. The object-oriented programmer instead starts out by asking "what can we do with the data?", which fosters the view that it is the data selectors that should be seen as foundational, and that the code of a data-producer consequently should be defined as an enumeration of implementations for these selectors."

What's telling is the description of the effects (local and non-local) of different types of modification to the program:

"The functional style ensures that adding another data-consumer (a function) is a strictly local change, whereas adding a new data-producer requires extending a datatype with a new constructor, and thus a major overhaul of every pattern-matching function definition. Likewise, in object-oriented programming the addition of a new data-producer (a new class) is a cinch, while the introduction of another data-consumer means the extension of a record type with a new selector, a global undertaking with implications to potentially all existing classes."

不是完全理解, 大义应该有很多相似的地方,
FP 中数据类型是根基, 很容易增加数据操作, 增加数据类型需要大范围调整
面向对象当中定义数据应有的操作, 增加操作会影响到大量数据拥有的方法(?)

麻烦的是在动态语言当中没有 Java 也没有 Haskell 那么多限制, 并不明确
所以看了上边的解释, 我其实还是有些糊涂, 虽然是我看到最明确的版本了
不过, immutable 那些特性可不是在这里边讲的,

另外还有一份 FP 的资料, 只看到 slide, 没找到视频
其中一张挺有意思, 就是 OOP 里各种范式, FP 都用函数解决掉了

我想说是的, 困惑我的, 是 FP 和 OOP 在思维方式上的巨大差别
而隐藏在后边的 immutable 特性, 在开始的时候更让我困惑
后面就开始说我的困惑和理解吧(确实网上的内容我没有理解透彻)

我的看法, immutability 是最重要的差别

FP 和 OOP 不是截然对立的思维, 在编写中实际上和经常相互渗透
OOP 当中, 比如动态语言随时可能插入 FP 的 List 操作方法
而 FP 当中, 也可能模拟 OOP 对数据封装和抽象(网上看到 Scheme 有例子, 不熟悉)
在 FP 当中, 很重要的概念是 Composable 和 Purity(可复合, 没有副作用)
但 OOP 当中并不是不允许组合操作和避免副作用, 同时这些是编程贯穿的主题
Node 脚本的代码, 模块化, 减少依赖, 建立在 OOP 之上, 说法和 FP 并没有太大区别
所以单纯在用法上的区别, 我任务应该是由更为基本的规则引起的

加上排除语法, 排除类型, 我还是觉得 immutability 是最为重要的差别
换句话说, 一个名字, 或者参数一致的函数, 每次调用的结果是否完全一样
FP 做了这样的限定, 导致编码的难度上升, 需要模拟每次操作后状态的改变
在 FP 中, 就是生成一份新的数据, 通过高阶函数, 模拟对同一个名字进行操作
但是首先, 共享变量名来进行消息传递这一点变得挺困难(或者说特殊)了

在 OOP 当中, 我们期待把大的问题拆解成几个小的模块分别解决
比如说, 为每个过程设定一些数据, 封装然后暴露一些接口, 出来
最终模块直接通过调用接口, 相互发送接受消息独立完成工作, 完成整个任务
考虑数据 immutable 的话, 每个模块内部状态就不能实现了
那么这部分状态将被提取出来放到全局, 再作为参数传回到模块当中
这时的模块更像是 pipe 的概念, 整个程序的运行就是在管道当中流动
一个好处是数据不再像 OOP 那样存在多份, 需要设计机制去同步

关于设计程序

后面是最近我在思考 Cumulo 实现的过程考虑到的和想到的问题
这是我第一次抛开框架写后端代码, 刚一开始被怎样组织代码所困扰
困扰之后我还是用 mutable 数据还消息来勾勒整个消息传播的方案
尝试之后我采用一个架构图描绘了数据怎样在这个程序当中流动

Cumulo

也许我可以这样思考程序, 将程序划分为几块, 分别对应功能:

  • 数据结构, 初始的资源和状态 --> 这是一个适合机器的形态
  • 交互的界面 --> 数据结构转化为适合人类编写, 调试, 查看, 交互的形态
  • 业务逻辑 --> 决定数据怎样改变的操作的规则
  • 架构 --> 操作或者事件, 怎样正确有效更新整个程序范围的资源和状态

程序越大, 思考架构越发困难, 特别是要照顾好多的细节, 细节相互冲突
简化来说, OOP 似乎很容易想采用消息, 让每个模块以监听的方式更新和同步
而在 FP, 没有消息, 转而采用定向的数据流动, 逐个地想做系统状态进行更新
现在我了解的, OOP 操作简单, 可是当模块繁多, 容易造成节奏紊乱, 难于调试
FP 则是不容易设计实现, 现实世界状态凌乱, 难以符合单向流的形式

另外我也注意到, 就是在微博上发的那条感想:

最近想起王垠那个比喻, 可变数据像 WIFI, 不可变数据像线, WIFI 方便多了. 可是从另一个角度看 WIFI 是短距离稳定, 远了信号差, 线麻烦是麻烦, 远距离传输损耗小(也不能普通网线...). 可变数据不用面向对象那样封装在局部进行控制, 变量多了真心容易乱. 不可变数据呢门槛又高..

mutable 数据, 如果被反复转发传送, 很可能会在某些操作当中被更改
当数据容易发生难以预料的更改, 构建大程序也就成了麻烦了
因此对于 mutable 数据, 在本地进行封装, 不直接对外暴露, 是极为合理的做法
这在 JavaScript 当中, 对象被不断引用共享, 直到出现 bug, 是很可能发生的事情
那么思考大程序的话, mutable 数据是应该尽量减少的方案

到目前为止, 我所能相对的构建程序最让我觉得建设性的是这个架构图
即便真心写到没思路了, 好歹还能照着图用拙劣的代码补上一些功能上去
对着 FP 和 OOP 这些争论了许久的功能思考不清楚, 只能按着图上瞎写
希望能早点走出这篇迷雾吧..


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者