
1. 被误读四十年的编程范式从Alan Kay的原话说起如果你在技术社区里抛出“面向对象”这四个字大概率会引发一场混战。有人会立刻搬出封装、继承、多态三件套有人会争论Java和C谁更OOP还有人会直接开喷“面向对象早该被淘汰了”。但有意思的是当我翻回去看Alan Kay本人对面向对象的原始表述时发现他说的东西和大多数人理解的完全不是一回事。Alan Kay是Smalltalk语言的核心设计者之一也是“面向对象”这个术语最早的提出者之一。他在多次访谈和邮件列表里反复澄清过一个观点他发明这个词的时候脑子里想的根本不是类、继承、抽象基类这些东西。他真正关心的是消息传递是独立实体之间如何通过通信完成复杂协作。用他自己的话说他当时受到生物学和早期分布式计算思想的启发觉得计算应该像细胞一样——每个单元独立运作通过发送化学信号来协调。这个认知落差非常大。我刚开始学编程的时候老师教的是“把数据和对数据的操作打包成一个类”考试考的是继承层次设计得合不合理。后来读了一些Alan Kay的访谈材料才发现他本人对C和Java那种以类为中心的范式其实颇有微词。他觉得那些语言把OOP带偏了变成了“抽象数据类型”的堆砌而不是他设想的“消息传递系统”。所以这篇文章我想做的事情很明确把Alan Kay关于面向对象的原始观点梳理清楚解释他为什么对后来的主流OOP实践持保留态度以及这些观点对今天的开发者到底有什么实际参考价值。不管你是刚接触编程的新手还是写了十几年代码的老手重新理解这段历史都会让你对“面向对象”这四个字有完全不同的认识。注意本文讨论的是编程范式层面的设计思想不涉及任何具体商业产品或平台评价。2. Alan Kay眼中的OOP消息传递才是核心类只是实现细节2.1 生物学隐喻细胞如何协作对象就该如何通信Alan Kay在多个场合提到过他构思面向对象的时候脑子里浮现的是生物体的运作方式。一个细胞不会去“继承”另一个细胞的内部结构它们各自独立通过释放信号分子来影响彼此的行为。这种模式放到软件里就是每个对象应该是一个独立的计算单元有自己的内部状态和行为逻辑对象之间通过发送消息来触发对方的响应。这个思路和后来主流的“类-继承”体系有本质区别。在类继承体系里你定义了一个父类子类继承它的属性和方法然后通过重写来实现多态。这种做法的关注点是代码复用和类型层次。但在Alan Kay的设想里关注点应该是运行时对象之间的动态交互。一个对象收到消息之后怎么处理完全由它自己决定发送方不需要知道接收方的内部实现。我举个实际例子来说明这个差异。假设你在做一个订单系统有订单对象和库存对象。在类继承范式下你可能会设计一个抽象的“业务实体”基类然后让订单和库存都继承它共享一些通用方法。但在消息传递范式下你更关心的是订单对象需要通知库存对象“有商品被下单了”库存对象收到消息后自己决定是扣减库存还是触发补货流程。发送方不需要知道库存对象内部是怎么实现的它只负责把消息发出去。这个区别听起来很抽象但它直接影响你的系统架构。消息传递范式天然适合分布式系统因为对象之间是松耦合的你可以把不同对象部署在不同的进程甚至不同的机器上只要消息能送达就行。而类继承范式更适合同一个进程内的代码组织一旦跨进程就会遇到各种序列化和接口兼容问题。2.2 为什么Alan Kay对Java和C的OOP实践不满意Alan Kay在2003年的一次邮件列表讨论中说过一段很直白的话大意是他发明“面向对象”这个词的时候C还不存在他想的根本不是C那种东西。他觉得C和后来受它影响的Java把OOP变成了“抽象数据类型”的同义词而真正的面向对象应该是“面向消息”的。这个批评的核心在于C和Java把类作为一等公民把继承作为核心机制导致开发者花大量时间在设计类层次结构上。你会看到很多项目里出现五六层深的继承链子类重写父类的各种方法代码变得极其复杂。Alan Kay认为这种设计方向偏离了OOP的初衷因为真正的灵活性应该来自对象之间的动态消息传递而不是编译期确定的类型层次。还有一个关键点是状态管理。在类继承范式里对象的状态通常是通过成员变量暴露给子类的子类可以直接读写父类的protected字段。这种做法在Alan Kay看来是有问题的因为它破坏了封装。他理想中的对象应该是完全自治的外部只能通过消息来影响它不能直接访问它的内部状态。Smalltalk在这方面做得更彻底对象的实例变量默认是私有的只有对象自己才能访问。2.3 Smalltalk的设计选择一切皆对象但对象之间只靠消息说话Smalltalk是Alan Kay参与设计的语言也是他心目中面向对象理念的最佳体现。Smalltalk有几个设计选择非常能说明问题。第一个是一切皆对象。在Smalltalk里连数字、字符、代码块都是对象。你写3 4实际上是数字对象3收到了一条消息参数是数字对象4。这个设计看起来很极端但它保证了整个系统的一致性——所有东西都遵循同一套消息传递规则。第二个是消息发送是唯一的交互方式。在Smalltalk里你不能直接调用一个函数也不能直接访问另一个对象的变量。你只能给对象发消息。对象收到消息后在自己的方法字典里查找对应的处理方法找到就执行找不到就触发doesNotUnderstand:机制。这个机制给了对象极大的灵活性你甚至可以在运行时动态添加或替换方法。第三个是没有类继承的强制要求。Smalltalk支持继承但它不是核心机制。你可以创建完全不继承任何父类的对象只要它能响应特定的消息就行。这种设计后来被称为“鸭子类型”——如果它走起来像鸭子叫起来像鸭子那它就是鸭子。你不需要一个抽象的“鸭子基类”来约束所有鸭子对象。这些设计选择加在一起构成了Alan Kay心目中的OOP一个由独立对象组成的系统对象之间通过消息协作每个对象对自己的行为负责系统整体表现出涌现式的复杂性。3. 消息传递范式在工程实践中的真实价值3.1 为什么松耦合的系统更容易应对需求变化消息传递范式最大的工程价值在于松耦合。当你把对象之间的交互定义为消息而不是方法调用时发送方和接收方之间就没有编译期的依赖关系。这意味着你可以独立地修改接收方的实现只要它还能响应同样的消息发送方就不需要重新编译。这个特性在需求频繁变化的项目里特别有用。我参与过一个电商促销系统的开发最开始的需求是“下单后扣减库存”后来业务方要求增加“下单后先预占库存支付成功后再实际扣减”。如果按照传统的类继承设计你可能需要修改订单类和库存类的接口重新编译整个模块。但如果用消息传递的思路你只需要让库存对象多响应一个“预占库存”的消息订单对象在合适的时机发送这条消息就行其他代码完全不用动。这种灵活性在微服务架构里体现得更明显。微服务之间的通信本质上就是消息传递——服务A发送一个HTTP请求或消息队列事件服务B收到后处理。服务A不需要知道服务B是用什么语言写的也不需要知道它的内部数据库结构。只要消息格式约定好双方就可以独立演进。这其实就是Alan Kay说的“对象之间通过消息协作”在分布式层面的体现。3.2 从Erlang到Actor模型消息传递思想的现代继承者Alan Kay的消息传递思想在Erlang语言里得到了很好的继承。Erlang的并发模型就是基于Actor的——每个Actor是一个独立的进程拥有自己的状态Actor之间只能通过发送消息来交互。一个Actor收到消息后可以修改自己的状态、发送新消息、或者创建新的Actor。这个模型的好处是天然支持高并发和容错。因为Actor之间不共享状态所以不需要锁也就不会出现死锁和竞态条件。一个Actor崩溃了不会影响其他Actor监督者Actor可以重启它。这种设计让Erlang在电信系统里跑了三十多年稳定性极高。后来Actor模型被Akka框架带到了JVM生态又被各种现代语言借鉴。比如Rust的Tokio框架虽然不叫Actor模型但它的任务之间也是通过消息通道来通信的。Go语言的goroutine和channel也是类似思路——不要通过共享内存来通信而要通过通信来共享内存。这句话现在被广泛引用但它的思想源头其实可以追溯到Alan Kay对消息传递的强调。3.3 消息传递与微服务架构的天然契合微服务架构的核心思想是把一个大系统拆成多个小型服务每个服务独立部署、独立扩展服务之间通过网络通信协作。这个架构风格和Alan Kay的OOP理念高度吻合。在微服务架构里每个服务就像一个对象——它有自己的内部状态数据库有自己的行为逻辑业务代码对外暴露一组接口消息处理端点。服务之间通过HTTP或消息队列通信发送方不需要知道接收方的内部实现。你可以用Java写订单服务用Python写推荐服务用Go写支付服务它们之间只要约定好消息格式就能协作。这种架构的挑战在于消息的可靠传递和服务的发现与路由。在单进程的Smalltalk环境里消息传递是语言层面保证的不会丢失。但在分布式环境里网络可能中断服务可能宕机消息可能重复或乱序。所以你需要额外的机制来保证消息的可靠传递比如消息队列的持久化、重试策略、幂等性设计等。这些工程挑战并没有否定消息传递范式的价值反而说明了它的核心地位——当你把系统拆成独立对象服务并通过消息协作时你获得的是架构上的灵活性和可扩展性代价是需要处理分布式系统的复杂性。这个权衡在今天的云原生时代依然成立。4. 重新理解封装、继承、多态哪些该保留哪些该扬弃4.1 封装的本意是保护对象自治而不是隐藏字段封装在主流OOP教育里通常被解释为“把字段设为private通过getter和setter访问”。但Alan Kay对封装的理解要深刻得多——封装是为了保护对象的自治性。一个对象应该完全掌控自己的状态和行为外部只能通过消息来请求它做事情不能直接干预它的内部。这个区别在实际编码中很重要。如果你给一个对象写了一堆getter和setter那这个对象实际上变成了一个“数据容器”外部代码可以随意读取和修改它的状态。这种做法破坏了对象的自治性导致业务逻辑散落在各个地方难以维护。更好的做法是让对象暴露行为接口而不是数据接口。比如一个银行账户对象不应该暴露getBalance()和setBalance()而应该暴露deposit(amount)和withdraw(amount)。外部代码只能请求账户执行存款或取款操作具体怎么更新余额、怎么记录流水完全由账户对象自己决定。这样业务规则就集中在对象内部不会泄露到外部。4.2 继承的问题强耦合的父子关系与脆弱的基类继承是主流OOP里被用得最多、也被批评得最多的机制。它的核心问题是强耦合——子类和父类之间是编译期绑定的父类的任何修改都可能影响所有子类。这就是著名的“脆弱的基类问题”你修改了一个基类的方法结果发现十几个子类的行为都出了问题。Alan Kay对继承的态度比较微妙。他并不完全反对继承但他认为继承不应该是OOP的核心机制。在Smalltalk里继承主要是用来共享方法实现的而不是用来建立类型层次的。而且Smalltalk的继承是单继承避免了多继承带来的菱形问题。现代编程语言的发展趋势也在印证这个观点。Go语言干脆不支持继承只支持组合和接口。Rust也没有传统的类继承而是用trait来实现行为共享。这些语言的设计者都意识到继承带来的耦合问题往往大于它带来的复用收益。组合优于继承这个原则现在已经被广泛接受而它的思想源头之一就是Alan Kay对消息传递和对象自治的强调。4.3 多态的本质同一消息的不同响应方式多态在主流OOP里通常被解释为“子类重写父类方法运行时根据实际类型调用对应实现”。这个解释没错但它把多态和继承绑得太紧了。在Alan Kay的框架里多态的本质要更简单同一个消息发给不同的对象它们各自做出不同的响应。这个定义不要求对象之间有继承关系。只要两个对象都能响应某个消息你就可以用同样的方式给它们发消息具体行为由对象自己决定。这就是所谓的“鸭子类型”或“结构化类型”。Python和JavaScript里的多态就是这样工作的——你不需要一个共同的基类只要对象有对应的方法就行。这种多态实现方式更灵活因为它不依赖编译期的类型层次。你可以给一个对象动态添加方法让它响应新的消息。你也可以用代理对象拦截消息转发给其他对象处理。这些技术在动态语言里很常见在静态语言里也可以通过反射和动态代理实现。从工程角度看基于消息的多态比基于继承的多态更容易测试和替换。你可以用一个mock对象替换真实对象只要它能响应同样的消息就行不需要关心它的继承层次。这让单元测试变得更简单也让系统更容易适应变化。5. 把Alan Kay的思想落地到日常编码5.1 从“设计类层次”转向“设计消息协议”很多开发者在接到需求后的第一反应是“我需要哪些类它们之间怎么继承”。这个思路在Alan Kay看来是本末倒置的。更合理的做法是先问“系统里有哪些角色它们之间需要交换哪些消息”我举个实际例子。假设你要做一个任务调度系统。传统思路可能是设计一个Task基类然后派生出定时任务、周期任务、一次性任务等子类。但消息传递思路会先问调度器需要给任务发什么消息可能是execute()、cancel()、getStatus()。任务需要给调度器发什么消息可能是onComplete()、onError()。把这些消息定义清楚之后再考虑每个消息由谁来响应、怎么实现。这个思路的好处是接口更稳定。因为消息协议是围绕业务交互设计的不会因为内部实现的变化而频繁修改。你可以随时替换任务的实现类只要它还能响应约定的消息就行。这在大型项目里特别重要因为接口的稳定性直接影响到团队协作的效率。5.2 用消息通道替代直接方法调用一个订单系统的重构案例我之前参与过一个订单系统的重构原来的代码是典型的类继承风格OrderService直接调用InventoryService的方法InventoryService直接调用LogisticsService的方法。整个调用链是硬编码的任何一个环节出问题都会导致整个流程失败。重构的时候我们引入了消息通道。订单服务不再直接调用库存服务而是往消息队列里发一条“订单已创建”的消息。库存服务订阅这条消息收到后执行库存扣减然后发一条“库存已扣减”的消息。物流服务订阅这条消息收到后安排发货。整个流程变成了事件驱动的每个服务只关心自己需要处理的消息不关心消息是谁发的。这个重构带来的好处很明显第一服务之间解耦了库存服务暂时不可用也不会阻塞订单创建第二流程更容易扩展后来要加一个积分服务只需要让它订阅“订单已创建”消息就行不用改订单服务的代码第三每个服务可以独立部署和扩展订单高峰期可以多部署几个库存服务实例来消费消息。当然这个方案也有代价你需要一个可靠的消息队列需要处理消息重复和乱序的问题需要监控消息的积压情况。这些是分布式系统的固有复杂性但换来的是架构上的灵活性和可扩展性。5.3 什么时候该用消息传递什么时候该用直接调用消息传递不是银弹不是所有场景都适合。我的经验是跨进程、跨团队、需要异步处理的场景适合消息传递同一个进程内、逻辑紧密相关的场景适合直接调用。举个例子一个用户注册流程里验证邮箱格式、检查密码强度、生成用户ID这些步骤是紧密相关的放在同一个服务里直接调用就行没必要拆成消息。但注册成功之后要发欢迎邮件、要初始化用户配置、要通知推荐系统更新用户画像这些步骤相对独立而且可能比较耗时就适合用消息传递异步处理。还有一个判断标准是失败处理。如果某个步骤失败了整个流程是否必须回滚如果是那这些步骤最好放在同一个事务里直接调用。如果失败可以容忍或者可以异步重试那就适合用消息传递。比如订单创建和库存扣减如果库存不足订单创建就应该失败这两个操作最好在同一个事务里。但订单创建和发送确认邮件之间就没有这种强一致性要求邮件发送失败可以重试不影响订单本身。6. 常见误解与容易踩的坑6.1 “面向对象就是写类”——这个误解从何而来很多人学编程的第一门语言是Java或C教材里第一章就讲“类是对象的模板”然后花大量篇幅讲怎么定义类、怎么继承、怎么重写方法。这种教学方式给人留下的印象就是面向对象编程等于写类。但Alan Kay的原意完全不是这样。他在访谈里说过他更愿意把面向对象称为“面向消息”或者“面向协作”。类只是实现消息传递的一种手段不是目的。你完全可以用函数式语言实现消息传递也可以用过程式语言模拟对象行为。关键不在于你用什么语法结构而在于你的系统是不是由独立实体通过消息协作构成的。这个误解的后果是很多开发者把大量精力花在设计类层次上却忽略了更重要的东西对象之间的交互协议是否清晰、消息的边界是否合理、系统的耦合度是否可控。结果就是代码里充满了复杂的继承关系但业务逻辑依然散落各处难以维护。6.2 过度设计类层次的典型症状与修复思路过度设计类层次有几个典型症状。第一个是继承链过深你看到AbstractBaseService下面有BaseBusinessService再下面有OrderServiceBase再下面才是OrderService。第二个是为了复用而复用两个类明明没有本质关系硬要抽一个共同父类出来。第三个是接口爆炸一个简单的功能定义了五六个接口每个接口只有一个实现类。修复这些问题的思路是回到消息传递。先问这个系统里有哪些角色它们之间需要交换什么消息然后根据消息来定义接口而不是根据类型层次来定义接口。如果一个对象只需要响应一个消息那就只定义这一个方法不需要为它创建一个接口。如果两个对象之间没有消息往来那它们就不需要有任何关系。我自己的经验是优先使用组合谨慎使用继承。组合的耦合度更低更容易替换和测试。继承只在一种情况下是合理的子类确实是父类的一种特殊形式而且父类的所有行为对子类都适用。如果只是为了复用几个方法就继承那不如把那几个方法抽成一个独立的工具类或者mixin。6.3 消息传递带来的新问题时序、幂等与可观测性消息传递范式解决了耦合问题但引入了新的工程挑战。第一个是时序问题消息是异步的你发出一条消息后不能立刻假设它已经被处理了。如果后续操作依赖前一条消息的处理结果你就需要某种机制来等待或轮询。第二个是幂等性在分布式环境里消息可能重复投递。如果你的消息处理逻辑不是幂等的重复处理就会导致数据错误。比如“扣减库存”这个消息如果被处理两次库存就会被扣两次。解决办法是给每条消息一个唯一ID处理之前先检查这个ID是否已经处理过。第三个是可观测性消息在系统里流转出了问题很难追踪。你需要日志、链路追踪、消息队列的监控面板来定位问题。这些基础设施在单进程应用里是不需要的但在消息驱动的分布式系统里是必须的。这些挑战不是消息传递范式本身的缺陷而是分布式系统的固有复杂性。好消息是现在有很多成熟的工具和模式来应对这些问题比如消息队列的持久化和重试机制、幂等性设计的通用模式、分布式追踪框架等。关键是要意识到这些问题的存在在设计阶段就考虑进去而不是等到出问题了再补救。7. 从Alan Kay的观点看现代编程语言的演进7.1 Go和Rust为什么放弃传统继承Go语言的设计者Rob Pike曾经说过Go没有继承是因为继承带来的耦合问题大于它带来的复用收益。Go用组合和接口来替代继承你可以把一个类型嵌入到另一个类型里来复用它的方法也可以通过接口来定义行为契约。这种方式更灵活因为组合关系可以在运行时改变而继承关系是编译期固定的。Rust走的是另一条路它用trait来定义共享行为用泛型来实现静态分发。Rust没有传统的类继承但你可以通过trait的默认方法来实现类似mixin的效果。Rust的设计哲学是“零成本抽象”它希望在不牺牲性能的前提下提供高级抽象能力。消息传递在Rust里通过channel来实现标准库的std::sync::mpsc提供了多生产者单消费者的通道。这些语言的设计选择反映了Alan Kay思想的影响对象自治、消息传递、组合优于继承。虽然这些语言的设计者不一定直接引用了Alan Kay的观点但整个行业在经历了多年类继承的痛苦之后正在向更灵活、更松耦合的方向演进。7.2 函数式编程与OOP的融合消息传递作为桥梁函数式编程和面向对象编程经常被对立起来但如果你从消息传递的角度看它们其实可以很好地融合。函数式编程强调不可变数据和纯函数面向对象强调封装和消息传递。两者的共同点是都反对共享可变状态。在Scala和Kotlin这些多范式语言里你可以用case class来定义不可变的消息用模式匹配来处理消息用Actor模型来实现并发。这种风格结合了函数式的简洁和面向对象的组织能力。Akka框架就是基于这个思路设计的它用Actor来封装状态用不可变消息来通信用监督树来管理失败。这种融合的趋势说明Alan Kay的思想不是过时的历史遗迹而是对现代编程实践依然有指导意义的活思想。不管你用什么语言、什么范式把系统拆成独立实体、通过消息协作、保护每个实体的自治性这些原则都是适用的。7.3 云原生时代消息传递思想在服务网格中的体现服务网格是云原生时代的一个热门概念它的核心思想是把服务之间的通信逻辑从业务代码里抽出来放到一个独立的代理层。每个服务旁边跑一个sidecar代理服务之间的所有通信都经过代理转发。代理负责服务发现、负载均衡、熔断、重试、加密等横切关注点。这个架构和Alan Kay的消息传递思想高度吻合。在服务网格里每个服务就像一个对象它只关心自己的业务逻辑不关心消息怎么路由、怎么加密、怎么重试。这些通信细节由代理层处理。服务之间通过消息协作发送方不需要知道接收方的具体位置和实现。这种架构的好处是关注点分离业务开发者只需要关注业务逻辑通信的可靠性、安全性、可观测性由基础设施层保证。这和Smalltalk里对象只需要关心怎么响应消息、不需要关心消息怎么传递的设计思路是一致的。虽然实现层面复杂了很多但核心思想是一脉相承的。8. 我个人的实践体会写了这么多年代码我对Alan Kay的观点有一个逐渐深入的理解过程。刚开始学Java的时候我觉得继承和多态是OOP的精髓设计模式里那些抽象工厂、策略模式、模板方法模式让我觉得很高级。但后来维护了几个大型项目之后我发现那些复杂的类层次结构往往是维护的噩梦。改一个基类的方法要花半天时间确认所有子类不受影响。加一个新功能要在继承链的某个位置插入一个新的抽象类。后来我开始尝试用更简单的方式组织代码优先用组合优先定义清晰的消息接口优先让每个模块独立自治。我发现代码变得更容易理解和修改了。一个模块的改动很少影响到其他模块因为模块之间只通过消息交互不共享内部状态。Alan Kay有一句话让我印象很深大意是真正的面向对象不是关于类的而是关于消息的。这句话我花了很长时间才真正理解。现在我在设计系统的时候会先画一张消息流转图标出每个角色需要发送和接收哪些消息然后再考虑怎么实现这些角色。这个顺序的调整让我的设计更清晰也更容易和团队成员沟通。当然Alan Kay的观点也不是教条。他本人也说过Smalltalk的设计有很多不完善的地方后来的语言在很多方面做得更好。重要的是理解他提出的核心原则——对象自治、消息传递、松耦合——然后根据具体场景灵活应用。这些原则在今天的微服务、Actor模型、事件驱动架构里依然闪闪发光说明它们触及了软件设计的本质问题。如果你正在为一个复杂的继承层次头疼或者觉得代码越来越难以维护不妨试试从消息传递的角度重新审视你的设计。先问“谁需要给谁发什么消息”而不是“我需要哪些类”。这个视角的转换可能会给你带来意想不到的清晰感。