再谈面向对象

一提及面向对象,“封装、继承、多态”立刻就浮现出来。在学习OO时,我们总是不假思索地接受了这些概念,却没能更进一步地思考,什么才是面向对象。

Motivation

首先要明确的是,编程范式,到底是人们对程序进行建模抽象的手段,哪怕不依赖面向对象、函数式编程,单单依靠计算机最基础的过程式——也即图灵机模型,也可以解决大多数问题。但正如我们不可能通过二进制的形式来直接书写文字、编辑音乐一样,我们需要一些更符合思维逻辑的方式来编写程序,否则当程序规模超过人们掌控范畴时,将难以理解与维护。

What

函数式编程将编程逻辑变为自顶向下的函数拆分,通过数学的逻辑演算与不变性来保障了程序的正确性;而面向对象则是从更为通俗的分工合作来构建程序的:我们有许多实体,其间通过消息传递来实现交互,进而构建出一个大型程序。

实体的概念使我们不需要完全了解其内部实现——其最内部往往还是需要按照面向过程来构建,即指令序列,保证了程序模块/部分的可替换性。而消息传递的概念则是我们协调各个实体的工具。

还需要明确的一点是,面向对象,是一种模式,语言可以提供便于实现该模式的特性,但模式绝不与语言特性绑定。一个很好的例子就是,linux操作系统内核中的各个模块,其间相互仅暴露有限的接口,用于消息传递,而其开发使用的却是不提供面向对象特性的C语言。另一方面,消息传递不一定通过本地的函数调用,现代互联网的微服务架构中,使用HTTP、RPC互相连接的微服务网络,又何尝不是一个庞大的面向对象系统呢?

History

了解了什么是面向对象的实质,我们便不得不好奇,所谓的“封装、继承、多态”是从何而来。

Two way

面向对象语言的发展统共可以分出两条脉络:

  1. Simula语言,以及一众追随其发展而出现的C++、Java、C#等通过继承结构实现代码复用与归一化的语言。
  2. Alan Kay设计的的Smalltalk,以及Swift、Obejective-C等并不使用继承结构的语言。

前者以演进的继承层次结构,将对象分门别类,使得子类默认复用父类代码,而子类也应当能够完成父类能够完成的事情。这种实现,直觉上十分符合现实,但是却由于缺乏函数式般的严谨,而容易导致谬误——“正方形是不是长方形?”,直觉上是,但在接口的一致性方面,正方形不是长方形。这就加重了开发人员进行设计时的心智负担;而默认启用的继承,也使得类之间继承关系的繁杂,多继承时的字段矛盾,也常常带来问题——这就是老生常谈的组合优于继承的由来。其主要问题在于,将实现的继承接口的继承进行了耦合。有时,我们仅需要两个功能类似的函数,而不在意其族谱;有时,我们想代码复用,但并非以方法的形式!

再就是多态。我们知道,严格的继承结构,限制了我们能够给怎样的对象发送消息,而静态类型的语言,往往又有参数、引用类型上的限制,这就使得这种结构下的语言不得不想方设法,使用重写、泛型、重载等方式来解决这些问题,于是,我们可以说,多态不过是继承的副作用。

从后者的角度看,问题就截然不同了。

“I made up the term ‘object-oriented’, and I can tell you I didn’t have C++ in mind.” ~ Alan Kay, OOPSLA ‘97

在Alan Kay看来,C++并不是其心中面向对象编程的样子。

“OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.”~ Alan Kay

类、继承关系绝非OOP的关注点,消息传递才是。在Smalltalk中,任何元素都可以是对象。

1
2
3
1 + 1

means send message + = 1 with param 1

即发送消息是十分自然的事情,不需要知道对象是什么,有什么接口,达成了更深程度的解耦。当然,这种随意的消息传递,比起模块开发内部,还是更适用于庞大的网络系统,毕竟这将为模块开发带来十分庞大的不确定性,不利于编译器检查。

Design Pattern

想要成为OO大师,设计模式是必经之路。试问谁在学习OO时没被23种设计模式所惊艳过呢?但其实绝大多数设计模式,其实本身仅仅是在弥补语言中的设计漏洞,譬如策略模式,不过是使用一个单独接口来实现函数引用;适配器模式,亦不过是为了绕过严格的某个类下的某个接口这种莫名严苛的规范。设计模式于OOP,与其说是设计的艺术,不如说是语言的补丁。

Continue Talk

在两种OO发展中,实体都具有不容撼动的地位,而消息的传递则各有各的做法。首先需要明确的是,大多编程语言都是原生支持单核单线程的,多数的并发编程都需要依赖显式引入的库函数。因此,在OO语言中的消息传递往往表现为对某个对象的函数调用。那么,我能调用哪些函数就显得尤为重要了。

在C++类语言中,我能调用的函数往往局限为某个类下的某个函数签名——即类、函数类型、函数名缺一不可;而在Smalltalk下,又似乎百无禁忌,什么都可以。前者限制过头,开发束手束脚;后者过于自由,愿景美好,但难以实现。

前者依靠多样的设计模式,缝缝补补,也算是撑到今天,然而也不免自我怀疑,吸收了函数式编程,以及interface这种纯接口的继承。

Statu quo

僵化的类型继承体系给开发者带来了过大的负担,使得开发人员不得不求助于各种技巧绕过类型继承限制。如今,新的开发语言已然开始抛弃继承体系,转而使用组合来完成代码复用,接口继承实现归一化;旧的语言也纷纷引入interface、protocol等特性来避免严苛的类型限制。

补充

类型继承机制对开发的限制在静态语言上能够得到比较深入的体现,而在动态类型下,其鸭子类型的特性,近乎天然地满足了OOP对无限制消息传递的可能性。在python与js中,其继承与其说是C++类的继承,不如说仅仅是实现上的继承,而接口继承的性质体现的却不是那么强烈,直至近来mypy、Ts将类型检查引入动态语言。

Reference

function/bind的救赎(上)

面向对象编程的弊端是什么

怎么从本质上理解面向对象的编程思想?

The Forgotten History of OOP