面向对象编程

过程式程序有一个结构性问题,不是语法,不是性能,而是数据的裸露

int balance = 1000;
int account_id = 42;
 
withdraw(&balance, 200);
deposit(&balance, 500);
audit_log(account_id, balance);

balance 漂浮在空中,任何函数都可以读它、改它。当系统规模增大,没人知道”谁在什么时候改了这个值”——这不是 bug,这是结构性的认知崩溃。

OOP 的起点是一个朴素的洞察:每一块状态都需要有一个负责人。 对象就是这个负责人——它保管状态,定义别人能对它做什么,并对状态的完整性负责。

把这个洞察推到极致:程序不是一系列指令,而是一个由相互通信的自治个体组成的社会。每个个体都有自己的状态和边界,通过消息协作,而不是共享数据。

一、对象是什么

三要素:状态、行为、身份

对象不只是”数据 + 方法的容器”。它有三个递进的要素:

  • 状态(State):我拥有什么——对象内部的可变数据,只有我自己能管理
  • 行为(Behavior):我能做什么——通过消息或接口暴露给外部的能力
  • 身份(Identity):我是谁——即使两个对象状态完全相同,它们仍然是两个不同的个体

身份是 OOP 区别于纯函数式数据的关键。{balance: 1000} 是数据;而一个银行账户对象,即使余额相同,仍然是可区分的个体,有自己的历史和责任。

Alan Kay 的原始定义:消息传递

OOP 这个词是 Alan Kay 在 Smalltalk 时代创造的,但他后来说:

“OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.”(对我来说,OOP 只意味着消息传递、状态的局部保持与保护,以及所有事物的极端迟绑定。)

他认为最重要的不是 class,而是消息传递:对象之间通过消息通信,发送方完全不知道接收方如何处理。

" Smalltalk:发送消息,不是调用方法 "
anAccount withdraw: 200.
维度消息传递(Smalltalk)方法调用(Java/C++)
发送方不知道接收方如何响应编译时绑定到具体方法
接收方自由决定如何响应,甚至可以不响应必须实现对应方法
灵活性支持 doesNotUnderstand 等动态机制类型不匹配则编译失败
性能需要运行时查找可以静态优化

主流语言实现的是方法调用,而不是真正的消息传递——这是 Kay 认为”OOP 走歪了”的地方。

两种对象模型:类式 vs 原型式

类式 OOP(Java/C++/Python/TypeScript):类是模板,对象是实例,继承在类之间发生。

原型式 OOP(JavaScript/Self):对象直接从对象克隆,无需类,属性查找沿原型链向上搜索。ES6 的 class 只是原型链的语法糖——底层仍然是对象到对象的委托。

原型模型更接近 Kay 的理念:对象是第一等公民,类不是必须的。

二、封装如何降低复杂度

认知模型匹配

OOP 之所以在大型系统中占主导地位,不是因为它理论上最优,而是因为它匹配人类认知的自然结构

人类理解世界的方式是名词 + 动词:银行账户(对象)、取款(消息)、余额(状态)。把这套认知模型直接映射到代码结构,降低了从需求到实现的翻译成本。这也是为什么 OOP 在领域建模中特别强大——业务实体(User、Order、Product)天然适合对象表示,业务规则天然适合方法约束。

封装的三个层次

封装不只是 private 关键字。它有三个递进的层次:

层次机制本质
物理封装private/pub/# 等访问控制编译器或运行时强制的访问门禁
逻辑封装模块/包级别信息隐藏明确哪些是内部实现,哪些是公开接口
语义封装接口契约(行为封装)调用方只关心”能做什么”,不关心”怎么做到的”

三层封装合在一起,实现了 David Parnas 1972 年论文中的核心主张:隐藏的不是数据,而是设计决策

一个 BankAccount 类不只是把 balance 藏起来——它隐藏的是”余额如何计算”、“透支规则是什么”、“日志记录在哪里”这些决策。这些决策的修改不会影响外部调用方,只要接口契约不变。

接口契约:承诺,而非实现

真正的封装意味着:你只需要知道对象能做什么,不需要知道它怎么做到的。

account = BankAccount(owner="Alice")
account.withdraw(200)  # 你不需要知道 balance 在哪里,怎么存储的

这让大型系统得以分工:实现团队和使用团队只需要对齐接口契约,不需要共享实现细节。这是 OOP 在工程组织上最大的价值。

三、状态是 OOP 的荣耀与原罪

OOP 的答案:局部化,而非消除

过程式程序的问题是状态无人负责,FP 的答案是消除可变状态,OOP 的答案是把可变状态锁在对象边界内,指定对象为责任人

class BankAccount:
    def __init__(self, balance: int):
        self._balance = balance
 
    def withdraw(self, amount: int) -> None:
        if amount > self._balance:
            raise InsufficientFundsError()
        self._balance -= amount

_balance 不再裸露在外,只有 BankAccount 自己能修改它,且只能通过定义好的入口。

原罪:共享可变状态

OOP 局部化了状态,但没有消除可变性。当多个对象需要协作,当线程需要共享资源,封装的边界就开始失效:

account = BankAccount(1000)
# 线程 A:account.withdraw(800)
# 线程 B:account.withdraw(800)
# 两者都通过了余额检查,余额变成 -600

这是 OOP 被批判的核心:

  • Joe Armstrong(Erlang 之父):“OOP 假设每个对象有一个共享的可变状态。这在分布式系统中是根本错误的。”
  • Rich Hickey(Clojure 之父):可变状态是并发 bug 的根源,对象封装只是把问题藏起来,没有解决问题。

自我修正:不可变对象与值对象

面对这些批判,OOP 语言逐渐引入了不可变性:

from dataclasses import dataclass
 
@dataclass(frozen=True)  # 不可变值对象
class Money:
    amount: int
    currency: str

不可变对象没有状态变化,可以安全地在线程间共享。现代 OOP 的最佳实践已经是:尽量用不可变对象,只在必须时允许可变状态,并明确标记它——这是 OOP 向 FP 借鉴的部分。

四、从对象到系统

继承:三种用途,一个陷阱

继承有三种用途:

  1. 代码复用:子类重用父类的实现(最常见,也最容易滥用)
  2. 接口契约:子类承诺实现父类定义的行为规范
  3. 类型层次:建立”is-a”关系,支持多态替换

核心陷阱是脆弱基类(Fragile Base Class):父类修改内部实现,子类行为意外改变。继承暴露了实现细节,创造了最深度的耦合。

Liskov 替换悖论Square 在几何上是 Rectangle 的子类,但在 OOP 中无法安全替换:

class Rectangle:
    def set_width(self, w): self._width = w
    def set_height(self, h): self._height = h
 
class Square(Rectangle):
    def set_width(self, w):
        self._width = w
        self._height = w  # 正方形宽高必须相等,破坏了 Rectangle 的行为契约
 
def stretch(rect: Rectangle):
    rect.set_width(rect.width * 2)
    assert rect.area() == rect.width * 2 * rect.height  # Square 让这个断言失败

几何上的”is-a”和行为上的”可替换”是两件事。继承绑定的应该是后者,不是前者。

组合优于继承

解决继承陷阱的答案是组合:has-a 替代 is-a,接口替代实现复用。

# 继承:紧耦合,LoggingAccount 依赖 BankAccount 的内部实现
class LoggingAccount(BankAccount):
    def withdraw(self, amount):
        super().withdraw(amount)
        self._log(amount)
 
# 组合:松耦合,持有依赖而不是继承它
class BankAccount:
    def __init__(self, logger: Logger):
        self._logger = logger
 
    def withdraw(self, amount):
        self._balance -= amount
        self._logger.log(amount)

Rust 走得更远——彻底删除继承,只保留 trait + 组合,证明继承不是 OOP 的必要条件,只是一种实现手段。

多态:同一契约,不同实现

多态是继承真正有价值的部分——它允许用统一的接口处理不同的实现:

类型机制特点典型语言
子类型多态继承 + 重写运行时分派,is-a 关系Java、C++、Python
参数多态泛型/模板编译时展开,类型参数化Rust、Haskell
特设多态重载/trait同一接口,不同类型实现Rust trait、Haskell Type Class
鸭子类型结构匹配运行时检查行为,无需声明Python、Go

SOLID:组合哲学的工程总结

SOLID 五条原则是”如何用组合而非继承构建系统”的工程化总结:

原则核心思想呼应上文
SRP(单一职责)一个类只做一件事对象是单一状态的负责人
OCP(开闭原则)扩展开放,修改关闭用接口和组合实现扩展,不改原有代码
LSP(里氏替换)子类必须可以替换父类Square/Rectangle 悖论的直接答案
ISP(接口隔离)小而专一的接口优于大而全接口契约越精确,耦合越低
DIP(依赖反转)依赖抽象而非具体实现组合时依赖接口,不依赖具体类

五、多语对照:不同哲学的工程落地

类与对象定义

维度PythonTypeScriptRust
定义方式class Dog:class Dog {}struct Dog + impl Dog
构造函数__init__(self)constructor()fn new() -> Self(关联函数)
类的地位类是一等对象,可运行时修改类是函数的语法糖无 class 关键字

继承与组合

维度PythonTypeScriptRust
继承模型单/多继承(C3 线性化)extends(单继承)无继承,trait 替代
多继承支持,解决菱形问题不支持,用 mixin 模拟不支持,多 trait 组合
组合方式属性持有属性持有 + mixinstruct 嵌套 + trait 实现

接口与多态

维度PythonTypeScriptRust
接口机制duck typing + ABC + Protocolinterfacetrait
类型策略结构化(鸭子类型)结构化类型标称类型
多态分派运行时查找运行时(可优化)静态分派 / vtable 动态分派

可见性控制

维度PythonTypeScriptRust
私有_ / __(约定)private / #(真私有)默认私有
公开默认公开publicpub
边界强度约定,无强制编译时检查编译器强制

三门语言的哲学倾向:

  • Python:信任程序员。鸭子类型让接口契约靠约定维护,灵活性优先,安全靠自律。
  • TypeScript:结构化类型适配 JavaScript 生态。形状即身份,接口不需要显式声明实现关系。
  • Rust:编译器替代继承。trait + 组合 实现了 OOP 所有价值,并消除了脆弱基类和共享可变状态两个核心问题。

延伸阅读

  • Kay, Alan. “The Early History of Smalltalk.” (1993) — OOP 定义
  • Gamma et al. Design Patterns: Elements of Reusable Object-Oriented Software. (1994) — 设计模式经典
  • Bloch, Joshua. Effective Java. (3rd ed., 2018) — 实践指南
  • Rust Book Ch10 — Trait 与泛型替代继承

关联 meta 维度