面向对象真正管理的,不是对象,而是依赖

📅 2026/7/7 11:07:49
面向对象真正管理的,不是对象,而是依赖
你自己可能见过蛮多代码一边写着面向对象一边把所有业务逻辑塞进Service对象只剩下一堆getter和setter。也见过为了一个配置查询搞出接口、工厂、策略、单例一整套结构代码看起来很「OO」维护起来却越来越复杂。这类代码的问题不是面向对象用错了而是方向就偏了。面向对象真正管理的不是对象而是依赖。这里说的依赖不是Maven里的依赖包而是软件中谁必须知道谁、谁必须跟着谁一起改。封装、抽象、多态、设计模式这些手段最终都指向同一件事让这些依赖关系变得可控。我们可以从OO的诞生历史开始看是不是这么回事。OO从哪里来面向对象的思想比面向对象的语言更早。很多人以为Smalltalk是第一个面向对象语言其实不是。第一个OO语言是Simula 67由Dahl和Nygaard开发的。他们在做运筹学研究需要精确描述和仿真复杂的人机系统。他需要一种语言既能编译成计算机程序也能让人读懂系统的结构。1967年Simula引入了继承机制从一种仿真语言变成了通用编程语言。对象、类、继承、虚函数这些OO的核心概念在Simula里都出现了。几年后Alan Kay在犹他大学读研究生时接触到了Simula的源码。他发现Simula的class结构很有意思于是写了Smalltalk。但Kay的目标不是做一种OO语言他要做的是Dynabook一个给儿童用的个人计算媒介。Smalltalk是他实现这个目标的工具。Kay对OO的定义和今天大多数人的理解完全不同。他说OOP对他来说只意味着三件事消息传递、状态的本地保护与隐藏、极致的晚期绑定。他还说过一句话我发明了object-oriented这个术语但我心里想的不是C或Java。到了80年代Stroustrup把Simula的关键概念引入了C语言创造了C。从Simula到Smalltalk到COO这条线索的脉络就很清楚了Simula为仿真而生Smalltalk是个人计算媒介的工具C是系统编程的需要。OO从来都是解决问题的手段不是目的本身。这个历史细节很重要。它告诉我们面向对象不是某种终极真理它只是几个不同团队在解决各自问题时不约而同采用的一种思路。无论是Simula、Smalltalk还是C它们关注的都不是「对象」本身而是如何组织一个越来越复杂的软件系统。于是一个问题自然出现了OO真正解决的到底是什么OO到底在解决什么问题很多人会觉得面向对象的核心是三大特性封装、继承、多态。这话说得没错但仔细想想封装并不是OO独有的。C语言用struct加配套函数本质上也在做类似的事情把相关的数据和处理数据的函数组织在一起。这是编程的基本习惯不是OO的发明。对于今天主流的Java、C#开发来说让OO区别于其他范式的最有价值的能力是多态。多态解决的核心问题是调用方和实现方之间的耦合。举个例子你在写一个开关要控制电器。不用多态的话你得在开关里写if-else判断电器类型publicclassSwitch{publicvoidturnOn(Lightlight,Fanfan,inttype){// 每增加一种电器这个方法就得改if(type1)light.on();elseif(type2)fan.start();}}每增加一种电器开关的代码就得改。用多态的话开关只依赖一个标准接口publicclassSwitch{publicvoidturnOn(Switchabledevice){// 新增电器不用改这段代码device.on();}}开关不需要知道接入的是灯还是风扇它只知道device.on()一定能被调用。新增一种电器只要实现Switchable接口开关的代码一行都不用动。这个能力的本质是源码依赖方向的反转。不用多态时调用方Switch的源码直接依赖实现方Light、Fan的源码。用多态后调用方只依赖接口Switchable不再依赖具体实现。实现方怎么变调用方都不受影响。这就是面向对象真正在做的事情管理依赖。SOLID原则里的依赖倒置原则讲的就是同一件事。不过话说回来这个能力不是免费的。如果只有一种电器引入接口就是多余的复杂度。多态的价值体现在一种操作需要对接多种不同的实现而且这些实现会持续增加。纯正的OO vs 变样的OO理解了依赖管理这个视角再来看实际项目中的代码就会发现很多写法虽然用了面向对象的语法但没有在管理依赖上起到任何作用。做了这么多年Java项目我见过太多代码看起来是面向对象的实际上跟面向对象的核心思想没什么关系。下面用三组对比来说明。贫血模型 vs 充血模型先看一个最常见的写法。一个Order类里面有totalAmount、discount这些属性加上一堆getter和setter没有任何业务方法。所有的折扣计算、库存校验、价格逻辑都写在OrderService里OrderService从Order对象里取数据在外面做判断再把结果写回去。这是典型的贫血模型。Order看起来是个对象实际上只是个数据容器。所有逻辑都在Service里本质上是在用面向对象的语言写过程式代码。当领域规则复杂时这种做法的问题会很明显花了OO的成本复杂的映射、多层抽象得到的却是过程式编程的结果。充血模型的写法不一样。Order自己负责业务逻辑publicclassOrder{privateBigDecimaltotalAmount;publicBigDecimalcalculateFinalPrice(){// 折扣规则、满减逻辑都在对象内部}}// Service只负责协调和调度orderService.save(order.calculateFinalPrice());数据和行为绑在一起了。Order自己知道怎么算价格Service只负责把结果持久化。这才是面向对象想解决的问题数据和操作数据的行为放在一起谁改数据规则就改这个对象不用满世界找Service。不过也不是说充血模型就是唯一正确的写法。当业务逻辑涉及到跨多个对象的协调时把逻辑放在Service层是合理的。数据和行为的协同定位是一种设计考量不是铁律。数据驱动 vs 过度OO再看一个场景。需要根据操作系统名称返回对应的提示信息。有人写出这样的代码// 接口 Factory Singleton 5个实现类BoxSpecifierspecOSDiscriminator.getBoxSpecifier();Stringmsgspec.getStatement();为了一个配置查询搞出了Factory、Singleton、接口、一堆实现类。每新增一个操作系统要加一个类还要去Factory里注册。同样的需求一个Map就解决了MapString,StringosMessagesMap.of(Linux,This is a UNIX box.,Windows,This is a Windows box.);StringmsgosMessages.getOrDefault(osName,Unknown OS);新增一个操作系统加一行数据就完事了。代码量少扩展容易可读性也好。有时候数据就是数据函数就是函数没必要什么都套进对象里。很多过度OO的设计一个共同特点就是本该用数据表达的东西被拆成了一堆类型。继承滥用 vs 组合优先继承被滥用的情况也非常普遍。为了复用代码有人建了很深的继承链// 为了复用公共逻辑所有导出器都继承BaseExporterclassPdfExporterextendsBaseExporter{}classExcelExporterextendsBaseExporter{}// 改BaseExporter所有子类全受影响父类一改子类全受影响。而且Java只支持单继承想同时复用两个父类的逻辑就不行了。用组合替代继承关系灵活得多// ExportService持有Formatter负责格式化// PdfExporter和ExcelExporter组合不同的Formatter// 改Formatter不影响导出器改导出器不影响FormatterGoF在《设计模式》开篇就提出了这个原则优先使用组合而不是继承。继承表达的是is-a关系组合表达的是has-a关系。现实中has-a的关系比is-a的关系更常见也更容易组合。OO真正擅长的场景说了这么多不适合的场景那面向对象到底擅长什么回到依赖管理这个视角来看就比较容易回答了。当一种操作需要对接多种不同的实现而且这些实现会持续增加时多态的价值就体现出来了。比如支付系统要对接支付宝、微信、银联、Apple Pay用接口加多态是最合适的做法。新增一种支付方式只要加一个实现类调用方的代码不用动。当数据和行为紧密耦合而且某个数据的处理规则复杂、会频繁变化时把数据和行为放在一起更方便维护。比如金融系统里的交易对象状态流转规则复杂把这些规则放在对象内部比分散在好几个Service里更不容易出错。不适合面向对象的场景也很明确。配置映射关系不适合一个Map或者配置文件就够了没必要给每个配置项建一个类。纯算法和数据流处理不适合排序、查找、数据转换这些操作用函数比用对象更直接。简单的CRUD业务不适合一个事务脚本就够了搞一套领域模型出来纯属折腾。一次性脚本更不适合写个脚本跑完就完事了设计接口和抽象类毫无意义。不是所有概念都自然地适合继承层次结构。很多关系根本不是层级的硬套进去只会让代码更难理解。Unix的启示Unix不是面向对象的系统但它在很多方面比OO系统更好地体现了面向对象的设计原则。组合优于继承、依赖接口不依赖实现、高内聚低耦合这些OO的设计准则和Unix的设计哲学单一、简洁、模块、拼装完全一致。Unix的管道就是Decorator模式ps -elf | grep javagrep修饰了ps的输出没有侵入ps的逻辑。Shell是Proxy和Facade代理了系统调用并提供统一的交互入口。/dev目录下把硬件设备适配成文件让你用read和write就能操作任意设备这是Adapter和Factory。思路和GoF那23个经典模式完全一样但Unix做得更优雅。管道就是一个竖线没有接口、实现类、工厂类这些层层包装。核心差异在胶合层的厚度。Unix的胶合层极薄薄得很优雅。而很多OO项目找一个业务逻辑要穿透七八层接口和抽象类点进去全是interface和abstract class。软件的复杂度有三个维度代码量、接口复杂度、实现复杂度。OO主要增加了接口复杂度。这恰好说明优秀的软件设计不依赖OO语法依赖的是同样的设计原则。OO最大的坑如果OO的目标是管理依赖那它最大的坑就在于太容易被用过头反而制造出更多的依赖。很多团队花了OO的成本建了完整的类层次结构、接口体系、抽象层结果所有逻辑都堆在Service里对象变成了纯粹的数据搬运工。这是最常见的一种情况用了OO的形式没用到OO的好处。还有一种是把简单问题复杂化。一个配置查询本来一个Map就能搞定非要搞一套Factory加Strategy加Observer出来。写的人觉得自己很专业维护的人苦不堪言。很多麻烦来自于对OO的理解不够深只学了形式。知道要用接口就到处定义接口。知道要用设计模式就硬套设计模式。知道要分层就分了七八层。至于为什么用接口、为什么用这个模式、为什么分这么多层说不清楚。面向对象没有错但它容易用过头。就像一把锋利的刀能切菜也能伤手。知道什么时候用、什么时候不用比知道怎么用更重要。什么时候该用OO什么时候不该用判断维度适合OO不适合OO数据与行为的关系关系复杂且会持续变化简单映射数据就是数据是否需要多态一种操作对接多种实现且实现会增加只有一种实现或很少变化领域模型复杂度有丰富的业务规则和状态流转简单的CRUD事务脚本足够变更频率处理规则经常变化需要局部修改逻辑稳定很少变更依赖管理需要调用方和实现方解耦调用关系简单直接代码生命周期长期维护的大型系统一次性脚本、临时工具这张表可以当作选型时的速查参考。遇到一个新的设计决策时对照一下这几个维度就能大致判断该不该用面向对象的方式来做。小结面向对象的价值不在于它提供了一种编程方式而在于它提供了一种管理依赖的思路。面向对象真正管理的从来不是对象而是软件中的依赖关系。多态让调用方和实现方解耦封装让数据和行为的变更局限在一个范围内组合让模块之间的依赖更灵活。这些手段最终指向同一件事而管理依赖的目的是让变化尽可能局限在一个小范围内。软件设计真正要管理的从来都是变化。技术选型没有标准答案只有适不适合。面向对象也好函数式也好过程式也好最终服务的都是解决实际问题。多态、继承、设计模式这些都是手段不是目的。手段为目的服务不是为了手段而手段。遇到一个新问题先想想这个问题本身的特点再决定用什么工具这个判断顺序比什么都重要。