1. 为什么“面向对象之父”这个头衔本身就值得聊一聊很多人第一次听到“面向对象之父”这个称呼脑子里浮现的可能是某个写了很多经典教材的教授或者某个发明了某门语言的大佬。但真正被业内公认戴上这顶帽子的是 Alan Kay。这个名字在编程圈里出现的频率不算高远不如那些语言创始人的曝光度但只要你往深了挖就会发现一个很有意思的事实今天绝大多数人嘴里说的“面向对象”跟他当年脑子里想的那个东西几乎不是一回事。我自己刚入行那几年对面向对象的理解就是“类、继承、多态”三件套写代码的时候先想怎么抽象一个基类再想怎么派生子类觉得这就是 OOP 的全部。后来读了一些关于 Alan Kay 的访谈和早期资料才发现自己一直在一个很窄的框里打转。他提出这个概念的时候核心根本不是“怎么组织类”而是“怎么让一堆独立的小东西互相发消息、协同完成一件事”。这个认知上的落差直接改变了我后来做系统设计时的思考方式。这篇文章想做的事情很具体把 Alan Kay 关于面向对象的原始观点拆开讲清楚他到底在说什么、为什么后来主流语言把它带偏了、以及我们今天写代码时能从中拿走什么真正有用的东西。适合已经写过一段时间代码、对 OOP 有一定实践但总觉得哪里不对劲的开发者也适合刚开始学编程、想从源头理解这个概念的人。我不会堆一堆学术词汇而是尽量用实际写代码的场景来解释让你看完之后能直接在自己的项目里用上。2. Alan Kay 眼中的面向对象到底在说什么2.1 核心不是“对象”而是“消息传递”Alan Kay 多次在访谈里强调过一个观点他后悔用了“面向对象”这个词因为大家的注意力全被“对象”吸走了而真正重要的是“消息”。这个说法第一次听到会觉得有点反直觉毕竟我们学 OOP 的第一课就是“万物皆对象”。但如果你回到他当年所处的环境就能理解他为什么这么说。上世纪六十年代末七十年代初计算机科学界正在摸索怎么处理越来越复杂的系统。当时的编程方式主要是写一大堆过程按顺序执行数据和处理逻辑是分开的。Alan Kay 受到生物学和早期分布式系统思想的启发觉得应该换一种思路把系统想象成一群独立的“细胞”每个细胞有自己的状态和行为细胞之间通过发送化学信号来协作。映射到软件里就是每个对象是一个独立的计算单元对象之间通过“消息”来通信而不是直接去读写对方内部的数据。这个区别非常关键。在“消息传递”的视角下对象内部怎么实现、用什么数据结构对外部来说完全不重要。外部只需要知道“我发一个什么消息给它它会回我一个什么结果”。这其实就是今天我们在微服务、Actor 模型、甚至前端组件通信里反复强调的“封装”和“接口隔离”只不过 Alan Kay 在五十多年前就已经把它作为核心理念提出来了。我举个实际写代码的例子。假设你在做一个订单系统有一个订单对象和一个库存对象。传统的 OOP 写法可能是订单对象直接调用库存对象的reduceStock方法甚至直接访问库存对象的某个字段。但在消息传递的视角下订单对象应该做的是“发送一条消息”给库存对象比如“我要扣减某个商品的库存”至于库存对象是直接改内存、写数据库、还是发给另一个服务去处理订单对象不需要知道。这个思路在今天的分布式系统里已经是标配但在单体应用里很多人反而忘了这一点把对象之间的耦合写得死死的。2.2 他为什么对“类”和“继承”保持距离Alan Kay 参与创造的 Smalltalk 语言里确实有类class的概念但他对“继承”这件事的态度一直比较微妙。他更看重的是“原型”prototype式的对象创建方式也就是通过复制一个已有对象来产生新对象而不是通过类来实例化。这个思路后来在 JavaScript 的原型链里得到了体现虽然 JavaScript 的早期设计跟 Smalltalk 的关系比较复杂但核心理念是相通的。为什么他对继承这么谨慎因为继承很容易导致一种问题子类被迫继承父类的一堆它根本不需要的东西而且父类的任何改动都可能影响到所有子类。这在大型项目里会变成一场噩梦。我自己就踩过这个坑。早年做一个后台管理系统设计了一个“基础实体类”里面放了创建时间、更新时间、创建人这些字段然后所有业务实体都继承它。一开始很爽代码复用率很高。但后来有一个实体不需要“创建人”这个字段另一个实体的“更新时间”逻辑跟其他实体完全不一样继承链就开始变得别扭。最后不得不把基础类拆成好几个接口用组合的方式重新组织才把问题解决掉。Alan Kay 更推崇的方式是“组合”和“委托”。一个对象需要什么能力就持有一个具备那个能力的对象把消息转发给它而不是通过继承去获得。这个思路在今天已经是设计模式里的常识但很多人写代码时还是会下意识地先想“我该继承谁”而不是“我该组合谁”。这个思维习惯的转变其实比学几个设计模式更重要。2.3 他真正关心的是“系统怎么长出来”如果把 Alan Kay 关于面向对象的所有论述浓缩成一句话我觉得是他关心的不是单个对象怎么写而是一整个系统怎么由大量独立单元组合而成并且能够灵活应对变化。这个视角在今天看来特别有现实意义因为我们现在面对的系统规模比当年大了几个数量级微服务、事件驱动、响应式编程这些概念本质上都在解决同一个问题怎么让一堆独立的部分协同工作同时保持各自的独立性和可替换性。他在访谈里提到过一个比喻大意是说一个好的系统应该像生物体一样每个细胞都知道自己该做什么细胞之间通过信号交流整个系统对外表现出稳定的行为但内部一直在更新换代。这个比喻放到今天的云原生架构里简直就是在说容器和服务的编排。你不需要知道某个服务内部是怎么实现的只需要知道它能响应什么请求、返回什么结果然后就可以把它替换掉、扩容、缩容整个系统依然正常运转。这个视角对我最大的启发是写代码的时候不要只盯着当前这个类怎么写要多想一步——这个类在整个系统里扮演什么角色它跟其他部分通过什么方式交互如果将来它的实现要换掉会不会影响到别人。这种思考方式比单纯追求“代码复用”要有价值得多。3. 主流语言把面向对象带偏了吗3.1 “类、继承、多态”三件套是怎么变成标准的如果你去翻任何一本讲面向对象的教材目录里大概率会出现这几个词类、对象、封装、继承、多态。这套话语体系主要是从 C 和 Java 这些语言的教学里沉淀下来的。C 早期为了兼容 C 并且提供高性能的抽象能力把类作为核心机制继承作为代码复用的主要手段。Java 更进一步几乎把所有东西都塞进了类里连一个简单的工具方法都要放在某个类下面。这套体系有它的历史合理性。在大型企业级应用里用类来组织代码、用继承来复用逻辑确实能解决一部分问题。但问题在于当这套东西变成“面向对象”的代名词之后很多人就以为 OOP 就等于写类、写继承。结果就是大量过度设计的代码出现为了继承而继承为了抽象而抽象一个简单的功能被拆成七八个类每个类里只有几行代码调用链长得让人头晕。我自己在维护一些老项目的时候经常看到这种代码。一个业务逻辑从入口到真正干活的地方要经过三四层继承每层都覆写了一点东西最后你根本不知道实际执行的是哪个版本的方法。这种代码的维护成本极高改一个小地方要翻遍整个继承树。Alan Kay 当年设想的“独立单元通过消息协作”完全不是这个样子反而更接近今天函数式编程里“组合小函数”的做法。3.2 被忽略的“消息”和“自治”主流 OOP 语言里“消息”这个概念其实是被弱化的。在 Java 里你调用一个对象的方法本质上就是直接调用编译器在编译期就确定了要执行哪段代码。这跟 Alan Kay 说的“发送消息”有本质区别。真正的消息传递应该是发送方把消息发出去接收方自己决定怎么处理甚至可以把消息转发给别的对象或者直接忽略。这种灵活性在 Smalltalk 和后来的 Erlang、Akka 这些系统里体现得更充分。“自治”也是被忽略的一点。Alan Kay 希望每个对象是一个独立的、有自己行为逻辑的单元而不是一个被动等待别人调用方法的数据容器。但在很多 Java 代码里对象就是一堆 getter 和 setter业务逻辑全在 Service 层里对象本身没有任何行为。这种写法被称为“贫血模型”在领域驱动设计里是被明确批评的。贫血模型的问题在于数据和行为分离之后任何一段逻辑都可能被散落在多个地方改一个规则要改好几个文件而且很容易漏掉。我并不是说所有项目都要用富领域模型有些简单的增删改查用贫血模型反而更直接。但如果你在做一个业务逻辑比较复杂的系统把行为放到对象内部、让对象自己管理自己的状态确实能减少很多不必要的耦合。这个思路跟 Alan Kay 说的“对象应该像一个独立的生物体”是一致的。3.3 从“是什么”到“能做什么”的转变Alan Kay 的观点里还有一个很实用的启发不要纠结于“这个对象是什么类型”而要关注“它能响应什么消息”。这个思路在今天的前端组件设计里特别有用。比如你设计一个按钮组件不要去想“它是一个 Button 类的实例”而要想“它能响应点击、能显示文字、能改变状态”。只要一个组件能响应这些消息它就可以被当作按钮来用至于它内部是用 div 还是用原生 button 实现的调用方不需要关心。这种“面向接口”而不是“面向实现”的思路在 TypeScript 里体现得很明显。你可以定义一个接口描述一个对象应该具备哪些方法然后任何实现了这些方法的对象都可以被当作这个接口来使用不需要显式地声明继承关系。这就是所谓的“结构化类型”跟 Alan Kay 强调的“消息决定一切”在精神上是一致的。我在做组件库的时候会刻意避免让组件之间产生继承关系而是通过 props 和事件来通信。一个组件需要什么数据就通过 props 传进去需要通知外部发生了什么就通过事件发出去。这样每个组件都是独立的可以单独测试、单独替换整个系统的灵活性会高很多。4. 把 Alan Kay 的思路落到实际项目里4.1 从“先设计类”改成“先设计消息”很多人的开发习惯是拿到需求之后先想有哪些实体每个实体有哪些属性然后画出类图再开始写代码。这个流程本身没问题但如果只停留在这一步很容易设计出一堆互相耦合的类。我的做法是在画类图之前先列一张“消息表”系统里有哪些角色每个角色会发出什么消息谁会接收这些消息收到之后会做什么反应。举个例子假设你在做一个简单的博客系统。传统的做法可能是设计 User、Post、Comment 三个类然后 User 有发布 Post 的方法Post 有添加 Comment 的方法。但用消息的视角来看应该是用户发出“发布文章”的消息文章服务接收后创建一篇文章读者发出“发表评论”的消息评论服务接收后创建一条评论并通知文章服务更新评论数。每个服务只关心自己接收什么消息、发出什么消息不关心其他服务内部怎么实现。这个思路的好处是当系统规模变大、需要拆分服务的时候你已经有了一张现成的消息流转图拆分起来会顺畅很多。而且每个服务的边界很清晰测试的时候也更容易模拟。4.2 用组合代替继承的实际操作如果你现在手上有一个继承层次比较深的代码想改成组合的方式可以按下面这个步骤来操作。第一步找出继承树里每个子类真正需要的方法把那些子类不需要但父类提供的方法标记出来。第二步把父类里那些通用的逻辑抽成一个独立的辅助类或者工具类。第三步让原来的子类持有一个辅助类的实例需要用到那些通用逻辑的时候通过调用辅助类的方法来实现而不是通过继承获得。这个过程听起来简单但实际操作的时候要注意一点如果父类的方法里用到了子类覆写的其他方法直接抽出来可能会改变行为。这种情况下需要把父类里那些会被覆写的方法也一起抽出来通过参数或者回调的方式传入。这个重构过程可能会比较繁琐但做完之后代码的灵活性会明显提升。我自己做过一次这样的重构把一个有五个子类的基类拆成了三个独立的辅助类子类通过组合的方式使用它们。重构之后新增一个子类只需要组合它需要的辅助类不需要再考虑继承树的结构。而且原来那些子类之间互相影响的问题也消失了因为每个子类都是独立的。4.3 消息传递在前后端通信里的应用Alan Kay 的消息传递思想在今天的前后端分离架构里其实已经得到了很好的体现。前端发送一个 HTTP 请求后端返回一个响应这就是一次典型的消息传递。前端不需要知道后端是用什么语言写的、数据库是什么只需要知道发什么请求、会得到什么响应。后端也不需要知道前端是用什么框架渲染的只需要按照约定的格式返回数据。但很多人在设计 API 的时候还是会不自觉地把后端的内部结构暴露出去。比如返回的数据里包含了数据库表的主键、外键关系或者 API 的路径直接对应后端的 Controller 方法名。这些做法都会让前后端产生不必要的耦合。更好的做法是把 API 设计成一组“消息”每个消息描述一个业务动作而不是一个数据库操作。比如用POST /orders而不是POST /createOrderFromCart用GET /users/{id}/profile而不是GET /getUserById。这个思路在 GraphQL 里体现得更彻底客户端可以精确地描述自己需要什么数据服务端按照这个描述返回结果双方通过一个 schema 来约定消息的格式。虽然 GraphQL 不是所有项目都适合用但它背后的“按需获取、消息驱动”的思路是值得借鉴的。5. 常见误解与实操中的坑5.1 “万物皆对象”不等于“万物皆类”很多人把“万物皆对象”理解成“万物皆类”觉得任何东西都应该先定义一个类然后再实例化。但在 Alan Kay 的原始设想里对象是运行时的实体类只是创建对象的一种方式而且不是唯一的方式。原型式编程里你直接复制一个已有对象修改它的几个属性就得到了一个新对象不需要先定义一个类。这个区别在实际开发里是有影响的。比如你在做一个配置系统需要根据不同的环境生成不同的配置对象。用类的方式你可能要定义 Config 类然后定义 DevConfig、ProdConfig 等子类。用原型的方式你可以先创建一个基础配置对象然后根据环境复制它并覆盖几个字段。后者在配置项很多但差异很小的情况下会更简洁。JavaScript 的对象字面量和展开运算符就是典型的原型式思路。你不需要定义一个类直接写一个对象需要变体的时候用展开运算符复制一份再改几个字段。这种方式在写配置、写测试数据、写临时对象的时候特别方便。当然对于需要复用行为、需要明确类型的情况类还是有它的价值。关键是不要被“必须用类”这个思维定式绑住。5.2 过度抽象比不抽象更可怕Alan Kay 强调消息和自治但有些人会把这个理解成“要把每个小功能都抽象成一个独立的对象”。结果就是系统里充满了各种细碎的对象每个对象只做一件极小的事情对象之间的消息传递链条长得离谱。这种过度抽象的问题在很多“设计模式驱动”的代码里都能看到。我见过一个项目一个简单的“用户登录”功能被拆成了 Authenticator、CredentialValidator、TokenGenerator、SessionManager 等六七个类每个类都只做一小步类之间通过接口和事件来通信。代码看起来很“优雅”但调试的时候非常痛苦因为你要在六七个文件之间跳来跳去才能搞清楚一个登录请求到底经过了哪些步骤。而且新增一个登录方式的时候要改好几个地方因为每个类都只负责一小块逻辑。抽象的目的是降低复杂度而不是增加复杂度。如果一个抽象没有让代码更好理解、更好修改那它就是一个失败的抽象。我在做设计的时候会问自己一个问题如果我把这个抽象去掉代码会变得更难懂吗如果答案是不会那我就不加这个抽象。这个判断标准虽然简单但能避免很多过度设计。5.3 消息传递不等于异步有些人一听到“消息传递”就想到消息队列、异步处理、事件驱动。但 Alan Kay 说的消息传递核心是“发送方不需要知道接收方怎么处理”而不是“发送方不需要等待接收方处理完”。同步调用也可以是一种消息传递只要调用方不依赖被调用方的内部实现。这个区分很重要因为很多人为了追求“消息传递”而强行把同步逻辑改成异步结果引入了不必要的复杂度。比如一个查询操作本来就是需要立即拿到结果的改成异步之后调用方要处理回调、要管理状态、要处理超时代码量翻了好几倍但并没有带来任何实际好处。我的建议是先判断这个交互是不是真的需要异步。如果调用方需要立即拿到结果才能继续那就用同步的方式但保持接口的清晰和独立。如果调用方不需要立即拿到结果或者需要同时触发多个操作那再考虑异步。不要为了“架构好看”而牺牲代码的简单性。6. 从 Alan Kay 的观点里能拿走什么6.1 设计时先想交互再想结构Alan Kay 的观点给我最大的启发是设计一个系统的时候先想清楚各个部分之间怎么交互再想每个部分内部怎么实现。这个顺序很重要因为交互方式决定了系统的灵活性和可扩展性而内部实现是可以随时替换的。具体到日常开发我现在的习惯是拿到一个需求先画一张草图标出有哪些角色、每个角色会发出什么请求、谁会响应这些请求。这张草图不需要很正式用纸笔或者白板画几个框和箭头就行。画完之后再考虑每个角色内部需要什么数据、什么逻辑。这样做的好处是系统的边界在一开始就比较清晰后面写代码的时候不容易出现“这个逻辑该放哪里”的纠结。这个习惯也影响了我做技术选型的方式。以前选框架的时候我会比较关注“这个框架提供了哪些类、哪些继承体系”。现在我会更关注“这个框架里各个部分怎么通信、消息格式是什么、能不能方便地替换某个部分”。后者才是决定一个框架能不能长期用的关键因素。6.2 保持对象的独立性和可替换性Alan Kay 说的“自治”落到实际代码里就是每个模块应该能够独立测试、独立替换。我在写代码的时候会刻意避免让一个模块直接依赖另一个模块的具体实现。如果两个模块需要通信我会定义一个清晰的接口让它们通过接口来交互。这样只要接口不变任何一个模块的内部实现都可以随时替换。这个做法在写单元测试的时候特别有用。如果每个模块都依赖接口而不是具体实现测试的时候就可以很方便地创建模拟对象不需要启动整个系统。我见过很多项目因为模块之间耦合太紧写一个单元测试要启动数据库、启动消息队列、启动一堆外部服务测试跑一次要几分钟。这种项目的问题不在于测试写得不好而在于模块的独立性太差。保持独立性还有一个好处是当某个模块需要重写的时候你可以放心地重写只要接口不变其他模块完全不受影响。这在长期维护的项目里价值巨大因为技术栈会变、需求会变能够局部替换而不影响全局是一种非常重要的能力。6.3 对“最佳实践”保持警惕Alan Kay 当年提出面向对象的时候并没有一套现成的“最佳实践”可以遵循。他是在探索一种新的思路而不是在执行一套固定的规则。这个态度本身就值得学习。今天我们在网上能看到大量的“最佳实践”、“设计模式”、“架构规范”这些东西有参考价值但不能盲目照搬。我自己的经验是任何一条“最佳实践”都有它的适用场景和前提条件。比如“面向接口编程”在需要灵活替换实现的场景下是对的但在一个简单的脚本里为了一个只用一次的功能定义一个接口就是多余的。“不要重复自己”在大多数情况下是对的但有时候两段看起来相似的代码背后的业务逻辑完全不同强行合并反而会导致耦合。我的做法是看到一条“最佳实践”的时候先问自己它解决的是什么问题我现在的项目有这个问题吗如果没有那我就不需要遵循它。如果有那我要理解它为什么能解决这个问题然后再决定怎么用。这种“先理解再使用”的态度比死记硬背规则要有效得多。7. 我个人的一些实操体会写了这么多最后分享几个我自己在实际项目里总结出来的小经验都是跟 Alan Kay 的思路相关的但更偏实操层面。第一个经验是当你觉得一个类的代码越来越长、越来越难改的时候不要急着去拆分类先看看这个类跟其他类的交互方式是不是有问题。很多时候问题不在于这个类本身太大而在于它承担了太多跟其他类通信的职责。把通信的部分抽出来让这个类专注于自己的核心逻辑代码自然就清晰了。第二个经验是在设计接口的时候尽量用“做什么”来命名而不是“怎么做”。比如calculateTotalPrice比sumItemPricesAndApplyDiscount要好因为前者描述的是目标后者描述的是步骤。用“做什么”来命名实现方式可以随时改用“怎么做”来命名实现方式就被固定下来了。第三个经验是不要害怕在项目初期把一些东西写“死”。Alan Kay 强调灵活性但灵活性是有成本的。在需求还不明确的时候过度追求灵活性会导致大量无用的抽象。更好的做法是先把功能实现出来等到确实需要灵活性的地方再引入抽象。这种“延迟抽象”的策略能避免很多前期过度设计的问题。第四个经验是多读一些老代码尤其是那些存活了十年以上的开源项目。你会发现这些项目里很少有一开始就设计得很完美的架构大多数都是经过多次重构、逐步演化出来的。Alan Kay 的思想在这些项目里往往体现得很明显模块之间通过清晰的接口通信每个模块内部可以独立演化。这种“演化式”的设计思路比“一次性设计完美架构”要现实得多。面向对象这个概念被讨论了五十多年各种解释和延伸层出不穷。但回到 Alan Kay 的原话核心其实很简单让一堆独立的小东西通过消息互相协作每个小东西自己管好自己的事情。这个思路在今天这个系统越来越复杂、变化越来越快的环境里反而比以往任何时候都更有价值。
阅读完成 · 觉得有帮助?