面向对象编程中的「对称性」

The flexibility provided by Smalltalk’s high degree of symmetry and extreme late binding have proven to help programmer productivity and creativity more than any theoretical benefits that might be derived from the use of static type checking, symmetry-breaking primitive data types or symmetry-breaking syntactic sugar.
(译文:Smalltalk 高度的对称性和极致的延迟(动态)绑定特性所带来的灵活性,帮助提升程序开发人员的工作效率和创造力。这种收益已被证明高于任何静态类型检查,对称性破缺的基础数据类型或者对称性破缺的语法糖所可能带来的理论收益。)

对称性的概念

对称性在几何和代数中有不同的定义。几何中,对称性是指对几何形体施加某种操作使它的位置能完全复原。代数中,对称性被描述为给定的组合定律下闭合,关联和可逆的一组变换。不论哪种功能定义我们都可以得出,对称性的核心即是变换条件下的不变性

分类(classification)与对称性(symmetry)

从认知科学的角度讲,我们人类的思考基于对事物进行分类的能力,分类最终建立起类(class)和实例(instance)的关联。一些该领域的人甚至认为分类支撑了人类的认知,并帮助构建了人类记忆存储的组织结构。在编程领域,面向对象编程语言通常支持两个层次的分类,即对象归类构建类之间的结构关系。基于此,有人认为所有面向对象的概念都可被统一于某种理论模型,并由分类学作解释。这也就提出从一种新的视角,即从生物学(biology)和分类学(taxonomy)的角度,来理解面向对象编程。

面向对象语言中,类是对象的分类。这种分类直接构建起了类的不变性——类的描述适用于该类的所有对象。从这个角度进一步推导,类便拥有了对称性:类赋予了对象多变性,但这种多变性需要遵循该类所定义的结构和行为。类的对称性带来的好处是,它为对象的多变性划分界限,且强制性地保证了类的正确性。

类的概念在基于组件的软件开发中非常有价值。组件可以按照如接口或者行为的兼容性等共有特征,分组为不同的类。于是类就能够用于创建具有共同特征,但各自不同的组件。当应用的需求发生变化时,只要新的组件遵循该组件类的共有特性,那么它就可以用于替换旧的组件。这种可替换性对基于组件的软件开发很有意义。另外,软件维护和演进过程中,替换过时或问题频出的组件已然令人头大的情况下,组件的可替换性也就同样重要。

更为重要的是,对称性和分类之间的联结,为面向对象软件设计提供了理论研究的基础。通过应用对称性原则,软件设计的目的就变成了最大化地识别和保留设计上的不变,同时能良好地支持变化。

接下来我们看另一种对称性。继承(Inheritance),大多时候会和一般化(generalization)与特殊化(specialization)概念挂钩,较少有研究者将之与分类(classification)相关联。面向对象编程中,分类上下文里,继承的角色不如类那样清晰。这是因为继承机制本身的灵活性。继承可以用以扩展类,限制类,或者修改类。这就意味着,子类和其超类可能并非包含关系,并且超类的描述也可能不适用于所有其子类。因此继承并不总能将类做出分类。但是,当继承用于子类型(subtyping)时,它就可以被看作是类的分类,也就保持了子类和其超类行为的一致性,亦即行为的不变性。此处需要明确一下,子类型只是继承扮演的角色之一。例如基于原型编程中对象的继承。因此,只有在子类型的概念下,继承方才用于对类进行分类。子类型和对称性的内联表现为:所有通过子类型方式构建的类,彼此可能大不相同,但一定保持并符合某种相同行为,或者说,它们是相同类型。

当然,我们可以进一步在面向对象编程中找到其他对称性的案例。但是以上两个例子就足以说明对称性在编程中的重要性。接下来,我们换一个视角进一步来了解对称性。

对称性的威力

对应到大众熟知的「面向对象编程」语言,如 Java,C++ 和 Python,「类和继承」的确被放在了核心位置。相应带来的对称性,当然也就体现在这些编程语言中。

作为对比,观察 Smalltalk 这一面向对象编程语言会发现,类,本质上是对象。该对象本身也是另外某个类(这种类被称为类的元类,metaclass)的实例。类是一个元类的唯一实例。所有的元类都是类 Metaclass 的实例。Metaclass 类是「Metaclass class」的实例,而后者又是 Metaclass 的实例。如此递归。Smalltalk 中类的编程只是一些操作数据的程序(program),这些程序本身又是数据(data)——可被其他程序操作。这不同于 Lisp 哲学(Lisp 因使用符号表达式,而让程序即为数据),但殊途同归。

甚至可以更进一步地说,Smalltalk 的对象/类系统的真正威力,不是它的继承特性,而是它的反射(reflection)机制。Smalltalk 运行在它被编写创造出来的上下文中(runs in same context it’s written in)。具体来说,Smalltalk 的运行时系统的元对象(metaobject)可以被具象化为普通的对象,被用以查询甚至查看内部细节。在 Smalltalk 中,元对象可以是类、元类、方法字典、编译过的方法、运行时栈,等等。

 

reflection

 

我们看到,Smalltalk 中体现出了上文所没有涉及到的另一个层次的对称——程序和数据的对称,数据即程序,程序即数据。这种对称在诸如 Java、C++ 和 Python 等面向对象语言中并不存在。甚至,Smalltalk 中体现出的这种独有的对称性,让其「类和继承」特性带来的对称性黯然失色,以至于类的继承仅仅是代码重用的方式。这种数据即程序的对称性,意味着,所有的信息都封装在即时运行的活(living)对象中。它使得 Smalltalk 调试器在程序执行过程中能够暂停,剖析,修改和恢复。这也是为什么 Smalltalk IDE 不仅仅是使用 Smalltalk 编写的,它也是 Smalltalk 语言本身。

后记

在查阅「面向对象」概念相关的文章时,数次出现了「对称(symmetry)」 这个词。让我疑惑的是,编程语言和对称有什么关系?进一步地调查发现,对称性的概念远远超越了设计范围,广泛存在于多种理论中。归根结底,对称性表现出来的哲学意义极具普遍性和指导性。当「变换条件下的不变性」这一个理念,和软件设计结合时,显得恰如其分。我们行业的前辈们,已然做了很多尝试,从编程语言的设计和软件开发的过程两个层面,最终想让软件开发更加轻松地应对变化。当然,这种努力今天仍旧在持续。

最后,希望本篇文章能够对大家有所启发,就像对我有启发。

 

参考文章:

Lisp, Smalltalk, and the Power of Symmetry

Understand symmetry in object-oriented languages

「面向对象编程」的讨论

撰写本文的初衷是因为最近看到了 Joe Armstrong 的文章,Why OO Sucks。文章写于上世纪九十年代,也正是面向对象编程这一概念风头正劲之时。在通读文章之后,我个人对 Joe Armstrong 的反驳观点其实并没有充足的认同。让我感到好奇的反而是为什么用这些论点反驳?我们都知道,Joe 是 Erlang 这门编程语言的创作者。而 Erlang 和 Elixir 两门语言的编程模型(Actor)以及对使用者心智模型的要求相较于面向对象编程有很大的不同。(关于这一点我会在以后的博客中做出说明。)Joe Armstrong 这一举动当然引发了众怒。但后来 2010 年 Joe Armstrong 在一次访谈(Ralph Johnson 也是嘉宾)中又说到:「Smalltalk 做对了很多事情。如果你问我关于面向对象编程的想法,不得不说经过这么多年我的观点已经有了一些改变。」他同时也坦诚了多年前写的文章(即 Why OO Sucks)「当时只是想以此挑逗下大家。这之后招致了很多非常有意思的反应,我也因此惹了众怒,即便我也确实我想达到的目的。」Joe 在访谈中也提到了 Alan Kay 非常有名的一篇澄清文章,即被误解的 OOP。Alan 作为 Smalltalk 这一个对面向对象编程语言影响甚大的语言设计者之一,在该文章中尝试纠正大家对面向对象在彼时彼景下的误解。当时编程人员过多的把注意力放在了类或者模块的设计,具体来说就是其内部的属性和行为该如何设计。但面向对象编程最核心的应该是「消息通信(messaging)」,原初的 Smalltalk 所使用的通用语都是一个对象发送消息到另一个对象,然后以消息的方式得到反馈。也正因为面向对象编程语言体系的精髓是消息,而 Smalltalk 当时并没有找到好的方式来支持消息的操作,因此 Smalltalk 一直被创作者们看作是持续开发中的状态,以期在未来不断达到更好更高的阶段。

当 Joe 在 2010 年思考面向对象编程(也就是前文中提到的访谈)时,反而认为 Erlang 才是最「面向对象」的编程语言。Joe 从 Alan 的面向对象澄清文章出发,谈论到了消息通信。在支持了消息通信的情况下,我们不需要关心消息的来源,编译系统会去负责消息的传送,我们也就不需要关心消息是如何被处理的。这种方式解耦了消息的发送者和接收者。而 Smalltalk 的问题之一便是它的对象或者模块从来没有真正地达到这种形式的隔离(isolationism)。理想情况下,因为相互隔离,一个程序内部的错误不会导致另一个程序崩溃。Java 就是一个例子。如果将两个 Java 程序塞到 JVM 里面,如果其中一个程序导致虚拟机宕机,不可避免的另外一个程序也会挂掉。(注:稍后的讨论中 Ralph 认为 Joe 的举例是另一个角度的隔离。)最后说到了多态(polymorphism),它和消息尤为息息相关。消息以指令或命令的方式告诉其他对象该做什么和如何完成任务。多态从定义上看,是,同样的消息到达不同的对象,意义不同,也就是做不同的事情。而这在 Erlang 中都有支持,因此 Erlang 才是最纯正的面向对象语言。

当然,Ralph 在访谈的后面也强调了,消息虽然强大而必要,但是如果缺乏良好的设计来保证消息通信以正确的方式被使用,那么这种隔离随之将失去意义。

下文中,我尝试翻译了 Alan 和 Joe 的文章,强烈大家通读,以作参考和启发。

Alan Kay
于 1998 年 10 月 10 日 04:40:35 UTC

诸位,

在此说明一下,上一次 OOPSLA,我颇花费了一些心思来让大家明白,Smalltalk 的核心不仅不是其语法或者类库,甚至不是它的类。很久以前我针对这一主题生造了「对象」这个术语,为此我感到非常后悔,因为这导致的结果就是让大家的关注点放在不那么重要的点上。

背后思想应该是「消息通信(messaging)」——这才是 Smalltalk/Squeak 的核心之所在(这也是我们在 Xerox PARC 阶段从未能实现的一部分)。日语中有一个简单的词—— ma ——意为「之间」—也许英文中「interstitial(间隙)」是意义最为接近的词。卓越且增长友好型系统的关键,在于系统模块之间如何交互,而非这些模块内部的属性或者行为该如何设计。以互联网为例,通过 1)允许有各种不同想法和意识,拒绝任何单一标准,2)同时为这些不同的想法提供彼此之间不同程度的安全互通,来保证互联网的持续存在和繁荣。

如果你把关注点聚焦在「消息通信」上面——同时明白一个优秀的元系统能够在将来结合对象中使用的各种二级体系结构—那么这一邮件主题下有关于编程语言,用户界面和操作系统的讨论都将没有任何意义。这就是我在上一届 OOPSLA 上抱怨的—然而在 PARC 我们经常性地对 Smalltalk 做些改动,从来都把它看作是仍在进行中的工作——当 Smalltalk 拥有更广泛的用户群时,它更多的被当作是「仅仅需要了解学习的东西」,就像是 Pascal 或 Algol。Smalltalk-80 从来都没有真正达到面向对象编程的下一更好更高阶段。结合编程普遍处于较低发展水平的现状,我觉得这一个错误尤为突出。

我记得当时也指出,构建的元系统的完整性显然至关重要,但构筑有助于防止越界的元边界的「栅栏」,其重要性也与之相当。一个最简单的例子,六十年代后期,激励我开启这趟旅程的原因之一:我意识到赋值操作是函数或方法的元层次的改变,因此不应该在函数层级去做—这也是把这一类状态的变化封装起来的原因之一,而非不管三七二十一一顿乱操作。我认为一个在一般编程过程中允许元层级抽象(metathings)改动(例如改变继承的含义,或者修改实例性质)的系统,从设计角度看是糟糕的。(我认同对一个系统而言,应该允许这些改动发生,但是设计上应该类如:当进行重大扩展时,原有的清晰边界应当被打破。)

我提议我们中优秀的头脑可以进一步思考元编程的未来,我们也定当取得更大成果。但我们怎样才能获得更多更大的力量,简约性和有意义的安全性呢?

谢谢大家,

Alan

面向对象为何糟糕

作者: Joe Armstrong (joe@bluetail.com) 

初次接触面向对象编程(OOP)这一个理念时,我是持怀疑态度的。但当时并不清楚具体原因—仅仅是直觉上它是「错误」的。随后 OOP 逐渐被普及并变得流行(原因我稍后会解释),当然同时也少不了例如,像是「在教堂咒骂」(译者注:表示亵渎的意思。参见 Desecration)之类的批评。但终究面向对象特性已然成为每一种重量级(respectable)编程语言的标配。

随着 Erlang 逐渐被大家熟知,我们也经常会被问道,「Erlang 是面向对象的嘛?」—真实答案肯定是「当然不是」—但我们当时并没在意需要对外澄清这件事情—我们巧妙地回答了这一问题,留给大家一种印象,即 Erlang (一定程度上)是面向对象的,但并不完全是。

此刻我想起了后来成为 IBM 法国的老板于巴黎举行的第 7 届 IEEE 逻辑编程会议上向观众的致辞。当大家问及关于 IBM Prolog 添加的很多面向对象编程的扩展时,他回复道:

我们的客户想要面向对象的 Prolog ,所以我们开发了面向对象的 Prolog 。

直到现在我都无法释怀这种「简单到可以昧着良心,毫不反思,甚至不问『这么做对吗?』……」的做法。

OO 为什么糟糕

我反对 OOP 的主要论点要回归到最基础的概念,我会列出其中一些并逐一反驳。

反对观点 1:数据结构和函数不应该绑定在一起

对象将函数和数据结构绑定在一起成为不可分割的整体。我认为基础性错误,因为函数和数据结构本来就天然地属于不同的世界。为什么这么说呢?

  • 函数是做某件事情的。它接受输入,然后输出。输入输出是被该函数作用于其上(例如修改)的数据结构。在大多数的编程语言里,函数由一连串的命令式语句构成。例如「先做这个,在做那个……」。理解函数,首先要理解完成某件事情的先后顺序。(在延迟计算的函数式编程语言中便无此限制。)
  • 数据结构就是数据结构。它什么都不做,纯粹用作声明。理解数据结构相比理解函数要简单太多。

函数可以被当作是黑盒(black box),它变换输入产生输出。如果我理解了输入和输出,我也就理解了函数。当然,这不是说我就可以直接编写函数了。

通常,在一个计算系统,函数负责将数据结构类型 T1 转化成数据结构类型 T2 。这也是我们所看到的和理解的函数。

函数和数据结构就像是完全不同的物种,因此你把它们锁在同一个笼子里面这种做法的从根本上就是错误的。

反对观点 2:一切都必须是对象

我们来看看「时间」概念。在面向对象语言中「时间」必须是一个对象。(在 SmallTalk 中,就连 「3」也是对象。)但在非面向对象语言里面,「时间」是一种数据类型实例。例如,在 Erlang 里,有非常多中不同类型的时间,通过如下方式可以给它们清晰明了地设值:

-deftype day() = 1..31.
-deftype month() = 1..12.
-deftype year() = int().
-deftype hour() = 1..24.
-deftype minute() = 1..60.
-deftype second() = 1..60.
-deftype abstime() = {abstime,year(),month(),day(),hour(),min(),sec()}.
-deftype hms() = {hms,hour(),min(),sec()}.

需要注意的是,以上这些定义不属于某些特定的对象。它们本身非常纯粹,任何函数都可以操作于这些表达时间的数据结构。

以上可以看出,「时间」并不需要任何相关的方法。

反对观点 3:面向对象编程语言中数据类型的定义散落在每个角落

面向对象编程语言中是通过对象来定义数据类型的。因此,并不存在一个集中的地方去获取到所有的数据类型的定义。在 Erlang 和 C 语言中,提供了导入文件或者数据字典的方式,让我们集中地定义所有的数据类型,而这在面向对象编程语言中便做不到—数据类型定义散布在各个角落。

举例来说。假设我需要定义一个全局(ubiquitious)的数据结构。

长久以来, Lisp 程序员喜欢使用少量的全局数据类型结合大量作用于其上的小函数,甚于使用大量的数据类型结合少量作用于其上的函数。

全局数据类型好比链表,或数组,或哈希表,或高级对象如时间,日期或者文件。

面向对象编程语言中,我们首先需要在某个基础对象中定义全局数据类型,之后任何其他需要使用这一数据类型的对象都必须继承这个基础对象。如果我现在想要创建某种「时间」对象,那么问题来了,它应该属于哪儿,置于何处呢?

反对观点 4:对象拥有私有状态

状态(state)是一切恶的根源。特别要避免有副作用的函数。

虽然在编程语言中状态不被喜欢,但是现实世界中状态可谓无处不在。我时刻都在关心我银行账户的状态,每次我存款或者取款之后,都需要看到更新后的账户状态是正确的。

既然状态在现实世界中是客观存在的,那么编程语言应该通过何种机制来处理状态呢?

  • 面向对象编程语言提倡「让状态对程序员不可见」。状态被隐藏起来,只有通过暴露出来的访问方法才可见。
  • 传统的编程语言(如 C ,Pascal 等)提倡状态变量的可见与否,应该被作用域所限定。
  • 纯声明式编程语言认为不存在状态。系统的全局状态在所有的函数之间流进流出。如 Monads 和 DCGs 之类的机制被引入,以达到让状态对程序员不可见的目的。如此,程序员能够编写代码「就像不用考虑状态一样」,但同时仍旧对必要的系统状态有完全的访问权限。

面向对象编程语言最糟糕的选择可能就是「让状态对程序员不可见」。它不去想着如何最小化避免因为状态可见引入的麻烦,它选择直接把状态隐藏起来。

为何 OO 广受欢迎

  • 原因一:被认为是容易学习。
  • 原因二:被认为能够让代码重用变得容易。
  • 原因三:被大肆宣传。
  • 原因四:因它而创造出新的软件行业。

并没有足够的证据可以论证原因一和二。原因三和四看上去像是技术背后的推动力。一项语言技术如果烂到能够创造出一个崭新的行业,而该行业就是去解决该技术带来的问题的程度,那它对于那些想着赚钱的人来说一定是绝佳的点子。

这也就是面向对象编程语言背后的推动力了。