1967 年,挪威计算机科学家奥利·约翰·达尔(Ole-Johan Dahl)和克里斯滕·尼高(Kristen Nygaard)发布了 Simula 67——一种为离散事件模拟而设计的语言(其前身 Simula I 早在 1960 年代初已问世)。为了模拟现实中的船只、银行和交通系统,他们引入了"类"和"对象"的概念:把数据和操作数据的方法打包在一起,称为一个对象。
这个实用的工程决策,最终演变成了 20 世纪最主流的编程范式。Dahl 和 Nygaard 因此分享了 2001 年的图灵奖,颁奖词写道:"表彰他们通过设计 Simula I 与 Simula 67 两门语言,为面向对象编程的出现奠定了根本性思想。"
Simula 的影响通过两条路径进入主流。一条是 Smalltalk:Alan Kay 在施乐 PARC 把 Simula 的"对象"与生物细胞的隐喻结合,做成了一门以消息传递为中心的语言。另一条是 C++:丹麦人 Bjarne Stroustrup 读博时用过 Simula,深知它建模能力强但运行太慢,于是 1979 年起在 C 上添加 Simula 式的类,做出 "C with Classes",1983 年改名 C++。今天 Java、C#、Python 中那个"把数据和方法装进类里"的写法,源头都能追到这台为模拟船只和银行而生的挪威机器上。
破除误解:OOP 的核心不是"继承"
许多教材把面向对象编程(Object-Oriented Programming,OOP)讲成:继承是核心,通过继承实现代码复用。这个理解导致了无数糟糕的代码库。
Alan Kay,Smalltalk 语言(Smalltalk-72 是第一版)的设计者,也是第一个使用"面向对象"这个术语的人,曾明确说过他对 OOP 的定义:消息传递(messaging)才是核心。对象之间通过发送消息通信,每个对象自己决定如何响应消息,内部实现对外完全隐藏。
在 2003 年的一封邮件里,Kay 把自己的定义压缩成一句话:"对我而言,OOP 只意味着消息传递、状态过程的局部保留与保护和隐藏,以及一切事物的极端晚绑定(late-binding)。"注意:继承和子类多态并不在这个清单里——发明这个词的人,根本没把继承当成 OOP 的必要成分。Kay 甚至说过:"我很后悔很久以前为这个主题造了'对象'这个词,因为它让很多人盯住了次要的概念。真正重要的是消息传递。"
这就引出一个最常见的误解:写了 class 不等于在做面向对象。把一堆函数塞进一个类、再用全局可变状态把它们串起来,本质还是过程式代码,只是换了层包装。OOP 的真正价值在多态与动态分发——让调用方对具体类型一无所知。反过来,没有 class 关键字的语言(如早期 JavaScript、甚至 C 配上函数指针)照样能实现这种"对象间发消息"的风格。
继承只是实现代码复用的一种机制,而且是问题最多的一种。父类一改,所有子类可能一起碎,这叫脆弱基类。组合把复用从"我是一种"换成"我有一个":车有引擎,不必是引擎的子类。GoF 的设计模式书后半本几乎都在教这件事。教材把继承画在第一章,生产代码里它往往是最后才动的刀。
OOP 的四个核心概念,重要性依次排列:
1. 封装(Encapsulation):把数据和操作数据的方法捆绑在一起,对外只暴露接口,隐藏内部实现。这是 OOP 最核心的贡献。
2. 多态(Polymorphism):同一个接口,不同类型的对象可以有不同的行为。调用者不需要知道对象的具体类型,只需要知道它能响应哪些消息。
3. 继承(Inheritance):子类复用父类的代码,并可以覆盖(override)部分行为。
4. 抽象(Abstraction):通过接口/抽象类定义"应该做什么",不规定"怎么做"。
多态:OOP 最强大的工具
// 不同类型的动物,但都能响应 makeSound()
Animal dog = new Dog();
Animal cat = new Cat();dog.makeSound(); // 输出:Woof cat.makeSound(); // 输出:Meow ```
多态的力量在于:调用者的代码(animal.makeSound())不需要随着动物种类的增加而修改。这是"对扩展开放,对修改关闭"(开闭原则)的直接体现。
多态有三种主要实现方式:
- 动态多态(运行时绑定):Java、Python 中通过继承/接口实现,方法调用在运行时决定
- 参数多态(泛型):C++ 模板、Java 泛型,一个函数可以作用于多种类型
- 特设多态(运算符重载):同一个运算符对不同类型有不同行为
Java 1995 年把虚拟机、垃圾回收和单继承接口模型打进浏览器,班级教材从此几乎只教一种 OOP。Python 的鸭子类型走另一条:不声明接口,只要会叫就算鸭子。Kay 的消息传递在动态语言里更近,在 Java 里被类型检查管得更死。没有唯一正确的对象,只有不同的绑定时机。
继承的问题:组合优于继承
继承是 OOP 中使用最广泛也被滥用最多的特性。《设计模式》(1994)一书最重要的设计建议之一就是:"优先使用组合(composition)而非继承(inheritance)"。
为什么?
脆弱基类问题(Fragile Base Class Problem):父类的修改可能以不可预见的方式破坏子类的行为。子类对父类的内部实现有隐式依赖。
继承层次地狱:多层继承(A 继承 B,B 继承 C……)产生的系统极难理解和修改。经典的反面教材是 Java 早期 GUI 框架的继承树。
违反里氏替换原则(LSP):如果子类不能在所有情况下替换父类,这个继承关系在语义上就是错误的。这条原则源自 Barbara Liskov 1987 年的 OOPSLA 主题演讲《Data Abstraction and Hierarchy》,她与 Jeannette Wing 在 1994 年的论文里把它形式化为"行为子类型"。经典反例是"正方形是矩形的子类"——听起来正确,但正方形强制宽高相等,会破坏所有依赖"矩形宽高可独立设置"的代码。类型层次若跟着小学几何走,运行时契约会碎。Liskov 问的不是名词像不像,是替换之后调用方会不会被惊到。惊到了,继承就是错的抽象,哪怕教科书封面印着"正方形是特殊的矩形"。组合在这里更老实:形状有宽和高,正方形是宽高被约束的值对象,不必住在矩形的派生树上。
组合的替代:用"has-a"(有一个)而不是"is-a"(是一个)来构建关系。一个 Duck 不需要继承 FlyingBird,而是持有一个 FlyBehavior 对象,在运行时可以更换飞行策略。
SOLID 原则
SOLID 是五条面向对象设计原则的合称。这些原则由 Robert C. Martin("Uncle Bob")在 2000 年的论文《Design Principles and Design Patterns》中系统提出;而 SOLID 这个好记的首字母缩写,是 Michael Feathers 在 2004 年前后发现五条原则的首字母恰好能拼成 "SOLID" 后才造出来的。换句话说,原则在先,名字在后。
| 原则 | 含义 |
|---|---|
| S - Single Responsibility | 一个类只负责一件事 |
| O - Open/Closed | 对扩展开放,对修改关闭 |
| L - Liskov Substitution | 子类必须可以替换父类 |
| I - Interface Segregation | 不应强迫客户依赖它不使用的接口 |
| D - Dependency Inversion | 高层模块不依赖低层模块,两者都依赖抽象 |
这五条原则不是 OOP 独有的,但在 OOP 语境下尤其重要。违反 SOLID 的代码库,通常就是难以测试、难以修改的"大泥球"(Big Ball of Mud)——这个词出自 Brian Foote 与 Joseph Yoder 1997 年的同名论文,他们坦率地指出:结构杂乱、用胶带和铁丝勉强糊住的意大利面式系统,恰恰是现实中最常见的架构,因为它是"在交付压力下做出能跑的系统"这一问题的反复出现的解法。原则是事后才能说清的方向,泥球是当场能交货的形状。骂泥球容易,按期在泥球里挖出可测的一角更难。OOP 既制造过泥球,也提供过把泥切开的刀。刀是封装和多态,不是 class 关键字本身。
OOP 与语言
主要的 OOP 语言形成了几个流派:
纯 OOP(一切皆对象):Smalltalk、Ruby——连数字都是对象,有自己的方法 混合 OOP:Java、C++——有基本类型(primitive types),不是对象 原型 OOP:JavaScript——没有类(ES6 class 是语法糖),对象直接从其他对象继承 多范式:Python、Scala、Kotlin——支持 OOP 和函数式
代价与争议
OOP 过度被神化:1990 年代,OOP 被视为软件危机的解药。实践证明,OOP 本身不能解决软件复杂性,糟糕的 OOP 代码和糟糕的过程式代码一样糟糕。
与函数式的张力:函数式编程的支持者认为,可变状态(OOP 的对象通常是可变的)是 bug 的主要来源,纯函数更容易测试和推理。两个范式各有优势,现代语言趋向融合。对象把时间藏进字段,函数把时间摊在参数上。藏起来的时间更难测,摊开的时间更难对领域专家讲。GUI 和游戏循环离不开可变;编译器和数据管道更爱不可变。选范式就是选哪一种时间更好说清楚。
Armstrong 的香蕉与大猩猩:Erlang 之父 Joe Armstrong 有一句广为流传的批评:"你想要一根香蕉,但你拿到的是一只抓着香蕉的大猩猩,外加它身后的整片丛林。" 他的论点是:面向对象语言里的对象总是隐式地拖着一大片环境(依赖、状态、上下文),你想复用其中一个方法,就被迫连带整个对象图一起拽过来;相比之下,引用透明的纯函数所有输入都来自参数、所有结果都从返回值出去、不留下任何状态,因此天然更容易复用。这并非否定 OOP,而是点出了它在"细粒度复用"上的结构性代价。
Java 的批评:Java 的强制 OOP(一切必须在类里)导致了大量"只有静态方法的工具类"这样语义错误但不得不用的模式。Martin Odersky(Scala 创建者)等人认为,过于僵化的 OOP 强制造成了不必要的冗余。Math.max 必须住在某个类里,不是因为最大值是一种对象,是因为语言语法没有顶层函数。约束会倒逼出假对象。假对象一多,教材上的"万物皆对象"就变成仪式。真正要问的是:这份状态值不值得藏进实例。不值得,就不要为了关键词去造类。
"面向对象是一种思维方式"的批评:OOP 通常被解释为"模拟现实世界"——车有颜色、狗能叫。但计算问题往往不是"模拟现实",强行用现实世界类比设计软件会产生扭曲的模型。Eric Evans 的领域驱动设计(DDD)是一种更成熟的"以领域为中心"建模方法。领域里的词要和代码里的类型对齐,不是把动物园搬进类图。狗能叫帮不了你设计支付状态机。支付有状态、有非法转移、有对账。对象要藏的是这些规则,不是皮毛颜色。把领域专家的句子写成类型名,比把现实名词写成继承树更接近 Kay 说的消息:系统里流动的是"请按规则入账",不是"我是一只狗"。模拟现实是广告,模拟规则才是工作。
消息传递的代价是间接:调用方看不见下一步跳到哪。测试和性能分析都更难。换来的是:新类型可以不改旧调用方就插进来。没有免费午餐,只有把修改的落点从"所有 switch"挪到"新类自己"。开闭原则说的是这个落点,不是让你永远不改文件。新需求来时,优先长出新类型去响应旧消息,而不是回去改所有分支。改不了的时候再打开旧文件。原则是默认方向,不是宗教。项目截止日期会强迫你改旧文件,那是债务,记下来下次再补多态。Simula 为了模拟港口而发明对象,不是为了让一切必须住在类里。起源是建模,教条是后来的。回到起源,问这份状态值不值得藏,比背 SOLID 口诀更接近工程。港口里的船是对象,因为船有自己的时间和队列。最大值不是对象,因为它没有时间。用有没有时间来判,假对象会少很多。没有自己时间的东西,做成函数或值就够,不必为了教材去继承。语法强迫万物进类,是语言史,不是本体论。
跨域连接
- 函数式编程:可变状态的代价可以说得很精确:同样的调用在不同时刻返回不同结果,等式推理随之失效,编译器与程序员都不能自由重排、缓存或并行这段代码。引用透明的纯函数没有这个问题,于是并发下不可变数据结构不需要锁,共享可变对象必须加锁——这不是风格之争,而是可推理性的直接后果。
- 本质主义:继承树预设对象有稳定的本质属性,因而可按属与种层级排列;里氏替换原则要求的正是子类不得违背父类承诺,正方形与矩形那个经典反例说明这种承诺常被形状之外的行为约束打破。推论是:当分类判据随语境改变,同一对象在不同场景归属不同类,继承树必然崩塌,组合才是出路。
- 生命之树与系统发生:生物分类学从按相似度分类转向按共同祖先分类,正因为形态相似可以由趋同演化产生,并不蕴含亲缘。软件里的继承层次犯的是同一个错:它同时承载共享实现与共享接口两件事,而这两者并不总是重合。把它们分开——接口用于替换,复用交给组合——才是「组合优于继承」的实质。
- 范畴论:泛型容器的映射操作是函子作用,它为「对同一形状的不同类型一致地作用」提供了精确刻画。这不是装饰性的类比:一个满足恒等与复合两条定律的实现,才能保证链式调用可被安全地合并或拆开。反过来,违反定律的映射会让重构在语义上不等价,而类型签名对此毫无提示。
- 设计模式:经典模式中相当一部分是在补继承的短板——策略、装饰、组合都在用组合替代继承。这给出一个可检验的判据:语言若原生支持某种能力,对应的模式就消失。一等函数使策略模式退化成传一个函数,多重派发使访问者模式失去意义。模式数量因此是语言表达力的负指标,而非设计水平的正指标。
参考文献
- Kay, A. The Early History of Smalltalk. ACM SIGPLAN Notices, 1993.
- Gamma, E., Helm, R., Johnson, R., Vlissides, J. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. ("四人帮",GoF)
- Liskov, B. Data Abstraction and Hierarchy. OOPSLA 1987 主题演讲(ACM SIGPLAN Notices)。
- Liskov, B. & Wing, J. A Behavioral Notion of Subtyping. ACM TOPLAS 16(6), 1994.
- ACM A.M. Turing Award, 2001 — Ole-Johan Dahl & Kristen Nygaard(颁奖词与传记,amturing.acm.org)。
- Stroustrup, B. A History of C++: 1979–1991. HOPL-II, ACM, 1993(stroustrup.com)。
- Foote, B. & Yoder, J. Big Ball of Mud. PLoP 1997(laputan.org/mud)。
- Martin, R. C. Design Principles and Design Patterns. objectmentor.com, 2000.
延伸阅读
- Martin, R. C. Clean Code. Prentice Hall, 2008.
- West, D. Object Thinking. Microsoft Press, 2004. (从 Alan Kay 的原始视角理解 OOP)