王垠:编程的宗派

总是有人喜欢争论这类问题 , 到底是“函数式编程”(FP)好 , 还是“面向对象编程”(OOP)好 。 既然出了两个帮派 , 就有人积极地做他们的帮众 , 互相唾骂和鄙视 。 然后呢又出了一个“好好先生帮” , 这个帮的人喜欢说 , 管它什么范式呢 , 能解决问题的工具就是好工具!我个人其实不属于这三帮人中的任何一个 。
王垠:编程的宗派文章插图
面向对象编程(Object-Oriented Programming)如果你看透了表面现象就会发现 , 其实“面向对象编程”本身没有引入很多新东西 。 所谓“面向对象语言” , 其实就是经典的“过程式语言”(比如Pascal) , 加上一点抽象能力 。 所谓“类”和“对象” , 基本是过程式语言里面的记录(record , 或者叫结构 , structure) , 它本质其实是一个从名字到数据的“映射表”(map) 。 你可以用名字从这个表里面提取相应的数据 。 比如point.x , 就是用名字x从记录point里面提取相应的数据 。 这比起数组来是一件很方便的事情 , 因为你不需要记住存放数据的下标 。 即使你插入了新的数据成员 , 仍然可以用原来的名字来访问已有的数据 , 而不用担心下标错位的问题 。
所谓“对象思想”(区别于“面向对象”) , 实际上就是对这种数据访问方式的进一步抽象 。 一个经典的例子就是平面点的数据结构 。 如果你把一个点存储为:
struct Point {double x;double y;}那么你用point.x和point.y可以直接访问它的X和Y坐标 。 但你也可以把它存储为“极坐标”方式:
struct Point {double r;double angle;}这样你可以用point.r和point.angle访问它的模和角度 。 可是现在问题来了 , 如果你的代码开头把Point定义为第一种XY的方式 , 使用point.x, point.y访问X和Y坐标 , 可是后来你决定改变Point的存储方式 , 用极坐标 , 你却不想修改已有的含有point.x和point.y的代码 , 怎么办呢?
这就是“对象思想”的价值 , 它让你可以通过“间接”(indirection , 或者叫做“抽象”)来改变point.x和point.y的语义 , 从而让使用者的代码完全不用修改 。 虽然你的实际数据结构里面根本没有x和y这两个成员 , 但由于.x和.y可以被重新定义 , 所以你可以通过改变.x和.y的定义来“模拟”它们 。 在你使用point.x和point.y的时候 , 系统内部其实在运行两片代码 , 它们的作用是从r和angle计算出x和y的值 。 这样你的代码就感觉x和y是实际存在的成员一样 , 而其实它们是被临时算出来的 。 在Python之类的语言里面 , 你可以通过定义“property”来直接改变point.x和point.y的语义 。 在Java里稍微麻烦一些 , 你需要使用point.getX()和point.getY()这样的写法 。 然而它们最后的目的其实都是一样的——它们为数据访问提供了一层“间接”(抽象) 。
这种抽象有时候是个好主意 , 它甚至可以跟量子力学的所谓“不可观测性”扯上关系 。 你觉得这个原子里面有10个电子?也许它们只是像point.x给你的幻觉一样 , 也许宇宙里根本就没有电子这种东西 , 也许你每次看到所谓的电子 , 它都是临时生成出来逗你玩的呢?然而 , 对象思想的价值也就到此为止了 。 你见过的所谓“面向对象思想” , 几乎无一例外可以从这个想法推广出来 。 面向对象语言的绝大部分特性 , 其实是过程式语言早就提供的 。 因此我觉得 , 其实没有语言可以叫做“面向对象语言” 。 就像一个人为一个公司贡献了一点点代码 , 并不足以让公司以他的名字命名一样 。
“对象思想”作为数据访问的方式 , 是有一定好处的 。 然而“面向对象”(多了“面向”两个字) , 就是把这种本来良好的思想东拉西扯 , 牵强附会 , 发挥过了头 。 很多面向对象语言号称“所有东西都是对象”(Everything is an Object) , 把所有函数都放进所谓对象里面 , 叫做“方法”(method) , 把普通的函数叫做“静态方法”(static method) 。 实际上呢 , 就像我之前的例子 , 只有极少需要抽象的时候 , 你需要使用内嵌于对象之内 , 跟数据紧密结合的“方法” 。 其他的时候 , 你其实只是想表达数据之间的变换操作 , 这些完全可以用普通的函数表达 , 而且这样做更加简单和直接 。 这种把所有函数放进方法的做法是本末倒置的 , 因为函数其实并不属于对象 。 绝大部分函数是独立于对象的 , 它们不能被叫做“方法” 。 强制把所有函数放进它们本来不属于的对象里面 , 把它们全都作为“方法” , 导致了面向对象代码逻辑过度复杂 。 很简单的想法 , 非得绕好多道弯子才能表达清楚 。 很多时候这就像把自己的头塞进屁股里面 。


推荐阅读