资讯动态

面向对象六大原则:从SOLID到迪米特法则的代码重构指南

发布时间:2026/10/20 12:08:30 来源:尧图企业网站定制
1. 从烂代码的痛说起接手过别人项目的人大多经历过这种崩溃瞬间一个类几千行改一个需求要顺藤摸瓜牵动十几个文件新增一个支付方式恨不得把原来的逻辑全推翻重写。这时候你会听到一个词——面向对象六大原则。说实在的这六个原则不是某个团队凭空发明的教条而是无数人踩坑踩出来的经验沉淀是判断一段代码“是不是该重构了”的通用标尺。这篇文章我打算换个讲法不只列定义而是把这些原则放进真实的开发场景里讲清楚它们解决什么问题、怎么落地、以及我最常看到有人把它们用歪的地方。不管你是刚学面向对象的新手还是写了两三年业务代码想系统性梳理一遍的开发者这篇文章都值得花十分钟慢慢看。先说人话版的六大原则单一职责、开闭原则、里氏替换、接口隔离、依赖倒置再加上一个常被单独拎出来的最少知识原则也叫迪米特法则。前五个合起来缩写正好是SOLID很多人叫它SOLID原则加上第六个习惯上统称六大原则。下面我会逐个拆但拆之前你得先有个整体认知这六个原则不是六个孤立的“规矩”它们分别管着“类内部怎么设计”“类之间怎么协作”“系统层面怎么规划”这三个层次。带着这个框架去看你会发现它们其实是一套自洽的方法论。2. 先搞清楚六条原则各自在解决什么问题2.1 单一职责原则类不想当万能工Single Responsibility Principle简称SRP。它的核心意思是一个类最好只有一个引起它变化的原因。听起来很简单对不对实际做起来几乎是我见过被误解最深的一条。很多人把它理解成“一个类只能干一件事”然后强行把业务拆得七零八落反而搞出一堆碎片化的类。正确的理解方式应该是一个类应该只有一个“责任”而这个“责任”指的是一组高内聚的行为它们会因同样的原因而一起变化。举个例子你立刻就明白了。假设你写一个订单类里面既有订单金额计算又负责把订单数据保存到数据库还负责生成订单确认邮件。表面上看它“都是围绕订单”但等你做了三个月产品经理说“邮件模板要改一下”你发现你要动订单类DBA说“订单表加个索引存储SQL要优化”你还是要动订单类财务说“折扣计算规则要调整”你又要动订单类。一个类被三个人从三个方向同时改每次改动都要重新测试全量功能这就是典型的职责混杂。正确的切法是把这三个关注点分离订单领域对象只管业务规则订单仓储只管持久化订单通知只管发邮件。关键判断标准不是“是不是同一类业务”而是“将来它们会不会因为不同的原因各自变化”。会就拆不会就别硬拆。2.2 开闭原则对扩展开放对修改关闭Open-Closed PrincipleOCP。这条原则说的是软件实体类、模块、函数等应该对扩展开放对修改关闭。我第一次听到这个说法时觉得像废话——既要改代码又不让改代码这不是自相矛盾吗后来才明白精髓这里的“不修改”指的是不修改已有类的内部实现而不是不写新代码。你想新增功能时应该尽量通过新增类、新增方法的方式去扩展系统而不是回头去动那些已经稳定运行的老代码。这就像家里的电源插座你要增加一个新电器不需要把墙拆了重新布线只要插头标准符合插座口够多直接插上去就行。老电器不会因为你买了新电器就要换插座新电器也能即插即用——这就是面向接口编程的朴素比喻。开闭原则是整个SOLID里最值钱的一条也是做架构设计时最该优先思考的一条。但它的代价也很明显想做到“对修改关闭”前期必须有足够好的抽象接口和扩展点得设计得对这就对设计者有比较高的要求。很多团队说“我们遵循开闭”实际上只是把接口定义了一堆新增需求时还是直接往老类里塞if-else这就是标准的嘴上说说。2.3 里氏替换原则子类不能坑爹Liskov Substitution PrincipleLSP。这条的定义很学术所有引用父类的地方必须能透明地使用其子类对象。说得通俗点如果代码里是拿父类类型去调用方法的那么换成任何一个子类实例程序行为都不该出错。这是继承用得对不对的试金石。经典的坏例子是“正方形继承长方形”。长方形有宽和高两个属性正方形也继承它逻辑上好像没问题。但父类里有个setWidth(5)的方法正方形子类为了保持边长相等只能重写setWidth顺便把高度也改成5。这时调用方用长方形的视角调用setWidth(4); setHeight(6)期望宽4高6结果正方形内部悄悄改成4和4数学结果就错了。子类没有维护好父类的“契约”这就是典型的里氏替换被破坏。现实中更常见的破坏方式是子类重写父类方法后抛出不支持的异常或者返回类型变了或者方法内部逻辑跟父类的约定不一致。这些问题的根源都一样——你在继承之前没想清楚“is-a”关系是不是真的成立。我自己的经验是如果你不确定一个子类设计是否合理就先问一句把它传给所有接收父类的方法会不会有方法内部假设被推翻会就别继承改用组合。2.4 接口隔离原则你不需要的东西别硬塞给你Interface Segregation PrincipleISP。意思是客户端不应该被迫依赖它用不到的接口。这句话得配个例子。你设计了一个“全能打印机”接口里面定义三个方法打印、彩色扫描、传真。大部分客户端只需要打印但因为它依赖了这个接口就必须实现全部三个方法。不会传真的模块被迫写一个fax()的空实现或者抛异常这就是接口污染。更务实的说法是接口要小而专。一个接口里的方法应该是一组调用方都需要的高内聚能力。别为了省事把所有能力堆进一个大接口里。在真实项目里这个原则经常表现为“接口分离比继承复用更重要”——比如把大接口拆成打印接口、扫描接口、传真接口需要什么就注入什么。通俗理解你去餐厅吃饭菜单分冷菜、热菜、主食、饮品几部分你只用看自己想点的部分而不是被迫把整本菜单研究一遍更不用为没点的菜付钱。接口隔离原则和单一职责有点表面相似但关注点不同SRP关注类的内部职责ISP关注外部调用者的依赖边界。很多时候你发现某个类又臭又长本质就是接口暴露得太多是ISP没做好的锅。2.5 依赖倒置原则高层不要依赖低层细节Dependency Inversion PrincipleDIP。定义是高层模块不应该依赖低层模块两者都应该依赖抽象抽象不应该依赖细节细节应该依赖抽象。绕口但这句话解决了项目里最痛的问题——分层架构里的“牵一发动全身”。假设你的业务层直接在代码里调用MySQL的查询API过两年团队决定把存储从MySQL换成别的数据库或者加一层缓存业务层代码就得跟着大改。问题出在业务层高层依赖了数据库操作低层的具体实现。依赖倒置的做法是业务层不关心数据来自哪里只面向一个仓储接口编程接口的实现可以是MySQL、内存、Mock数据将来想换就换。这套思路在现在的工程实践里已经无处不在Spring里编程式事务依赖接口而不是具体实现各种ORM框架、消息队列客户端核心设计思想都是倒置依赖。理解DIP之后你会自动理解为什么很多框架的官方推荐架构都长那样依赖往内聚抽象往外放实现永远在边缘。2.6 最少知识原则别隔空喊话Law of DemeterLoD也叫最少知识原则。它管的是“说话”的范围一个对象应该尽量避免了解其他对象的内部细节只跟“最亲近的朋友”通信。什么叫“最亲近的朋友”自己的成员变量、方法参数、自己创建的对象。最典型的坏味道是一连串的.getA().getB().getC().doSomething()这种链式调用叫“火车残骸”代码完全暴露了调用路径的内部结构任何一个环节改名整条链都断掉。最少知识原则不是让你不许调用别人而是让你不要“隔空”调用——你和它之间隔了多层间接关系就别去够它。这在设计模式里的门面模式、中介者模式里体现得很明显让中间的对象帮你完成跨组件的交互而不是每个对象都去摸全系统的内部结构。这个原则最容易被忽视因为写起来很爽但维护起来最痛苦。3. 六大原则不是六条独立规则而是一套组合拳很多人学完六大原则的单独定义后还是不知道实际项目里怎么组合用。这里我直接用一个真实的业务场景把它们串起来。比如你负责一个电商系统的订单模块需求是下单后要算优惠、扣库存、发通知。新手可能会写一个大OrderService所有逻辑全塞在里面——这几乎同时违反了好几条原则。按六大原则的思路我会这么设计第一层订单领域模型只负责自身的业务状态和金额计算这是单一职责。第二层把“扣库存”和“发通知”都抽象成接口订单流程面向接口编程而不是直接依赖具体实现——这是依赖倒置。第三层优惠策略设计成策略接口新增一种优惠只需要新增实现类不碰老代码——这是开闭。第四层库存接口、通知接口各自定义自己最精简的方法别让扣库存的实现类被迫实现“发送短信”这种它不关心的能力——这是接口隔离。第五层所有子类实现这些接口的时候严格按父接口的约定行事不抛异常不缩水——这是里氏替换。第六层订单服务只跟自己的仓储、策略、通知这些直接依赖协作不去操作仓储内部的数据库连接——这是最少知识原则。你看六个原则在这一套设计里各自管一段SRP管类的划分OCP管扩展方式LSP管子类契约ISP管接口边界DIP管依赖方向LoD管协作范围。顺口说一句SOLID之间的关系也很密切没有LSP做保障OCP就是空话——你看父类定义的扩展点被子类一支就崩了扩展个屁没有ISP把接口切干净DIP依赖的“抽象”就会变成一团浆糊因为它什么都装。这六个东西不是一个一个按顺序“检查”的而是在设计过程中像一套质检标准一样同时起作用。说到这我想起一个反例某同学接到需求说“加一个支付方式”他直接在原来的支付处理类里加了一个if (type 某支付方式)的分支。功能很快跑通了但三个星期后产品又说加另一个渠道这次他把整个方法复制了一份改成二代码瞬间膨胀。再到第三次他承认这个类已经没人敢改了。这个案例就是典型的“没有用开闭原则也没有用依赖倒置”——支付类型作为可变化的需求就应该被抽象成策略接口新渠道只是新增一个类的事情。4. 实操中怎么判断代码违反原则了4.1 你闻到坏味道的瞬间先别急着重构我这些年有个习惯做code review时不太上来就背原则而是先看代码里的“坏味道”信号。一旦闻到以下几种味道就该回头对着六大原则逐条比对了。一个类超过三百行方法超过八个命名里混着多个业务名词——大概率SRP破了。新增需求要修改老文件的比例大于新增文件比例——大概率OCP破了。子类重写方法时先抛异常、或者用instanceof判断再强转——大概率LSP破了。接口里一半方法在具体实现里都是空实现——大概率ISP破了。高层类直接 new 具体实现或者直接调用低层模块的静态方法——大概率DIP破了。代码里出现连续.get().get().get()——大概率LoD破了。注意闻到味道和立即改是两回事。我刚工作时特别激进看到不符合原则的代码就想重构结果把同事正在开发的功能搞乱了。后来才学会一个原则重构前先看看这块代码是否正在稳定运行、是否近期会频繁改动。如果一段代码虽然丑但是没人动它优先级就靠后如果是正在迭代的功能越早修正越便宜。4.2 不要为了“符合原则”而过度设计这是新手最容易掉进去的陷阱。很多开发者学完六大原则后写一个登录接口都要抽象出三个接口、五个类、两个工厂结果代码量翻倍维护成本反而更高。六大原则是指导你演化设计的不是让你一开始就堆抽象层。正确的做法有三个判断标准第一这段代码是否有多个变化方向不确定就先写简单实现出现第二次变化再抽象。第二扩展点是否已经明确了如果连需求都没想清楚预设的抽象大概率是瞎猜。第三团队里其他成员能不能看懂你的抽象如果一个抽象只有你自己能解释那它就不是在帮团队而是在制造新的理解成本。有个挺形象的类比六大原则就像驾照里的交通规则不是让你在空无一人的路上也每个路口都停三秒而是让你在复杂路况下不撞车。写单次脚本、一次性工具完全可以不理会这些原则怎么快怎么来。但只要是长期维护、多人协作的代码这些原则就是保命绳。4.3 一个实用的重构顺序当你确定一段代码需要重构时我建议按这个顺序来能少走很多弯路先处理依赖倒置——把高层和低层之间的具体依赖改成接口依赖这是改动风险最高的一步优先做因为它决定后续所有重构的结构基础。再处理单一职责——基于上一步拆出来的接口把大类的行为按职责分组拆出新的类。接着检查接口隔离——拆完类之后看新类的方法是否都是调用方真正需要的把不相关的方法从接口里挪走。然后验证里氏替换——所有重新组织过的子类跑一遍父类意义上的测试。很多隐蔽bug藏在这里。最后审视最小知识——看看有没有对象之间跨层调用能通过中间对象收口的尽量收口。每完成一步都跑一遍测试再进入下一步不要一次性堆完全部改动再跑测试。我见过不少人重构改到一半出了问题因为改动面太大根本定位不到是哪个环节引入的bug。5. 六大原则的组合实战从订单模块到报表导出前面讲了理论这里我完整拆一个基于六大原则的实战设计拿“报表导出”这个需求来说因为它在几乎所有后台系统里都会遇到而且特别容易写出违反原则的代码。假设需求是系统里要导出订单报表格式可以是Excel或PDF数据来源可能是MySQL或某个第三方接口。新手写法大概是一个ReportService类里面if (format.equals(excel))走一段导出逻辑else if (format.equals(pdf))走另一段数据源也是类似逻辑。这个类里全是分支将来每加一种格式、一种数据源都要改这个核心类。按六大原则的落地设计应该是怎么样的第一步定义两个接口ReportExporter负责导出格式和ReportDataSource负责数据来源。第二步分别实现ExcelExporter、PdfExporter、MySqlReportDataSource、ThirdPartyReportDataSource。第三步ReportService只面向这两个接口编程不关心具体实现是哪种。第四步通过依赖注入把这些实现装配进去。第五步所有实现类严格按接口契约返回数据不在背后搞小动作。第六步ReportService只调用接口方法不直接操作DataSource内部的连接对象或者格式化状态。这个结构写完之后你再新增一种CSV导出格式只需要新建一个CsvExporter实现ReportExporter接口然后改一下装配配置老代码一行不动——这就是开闭原则的直观体现。ReportService作为高层把控制权交给配置而不是自己new具体实现——这就是依赖倒置原则的体现。ReportExporter接口里只有export(ReportData)一个方法没有塞进sendEmail()这种它不该管的事——这就是接口隔离原则的体现。所有实现类返回的数据结构完全一致不会A实现返回ListMap、B实现返回JSON字符串——这就是里氏替换原则的体现。我在项目里真实落地过类似方案最深刻的体会是前期多花半小时设计接口后期真的能省下几天改bug的时间。而且这种结构有一个额外好处——测试特别好写因为接口分离后你在测试里可以轻松注入一个假数据源或者伪导出器每个模块单独验证不用为了测一个导出功能去准备真实数据库和真实文件生成环境。6. 六条原则常被误解的地方以及我的排查心得6.1 单一职责被误用为“一个类只写一个方法”有个初级开发曾把用户类拆成UserGetter、UserSetter、UserSaver三个类理由是“单一职责”。这是典型的把“职责”理解成了“操作”。一个用户类的职责是“管理用户信息”里面的getter、setter、save都是这个职责范畴内的不同操作不需要拆。判断到底该不该拆看的是“变化的原因”而不是“方法的数量”。我的排查心得是给类命名时如果能用一个业务名词来描述它的职责比如OrderService、PaymentValidator并且这个名字能覆盖类里所有方法表达的事务那就别拆如果名字越写越长需要两个名词才能描述全比如OrderAndInventoryManager那就该拆了。6.2 开闭原则被误用为“禁止修改任何已有代码”有一次我带项目产品提了个合理的小需求要在原有流程里加一个字段校验。我手下一位按“开闭原则”拒绝改老类非要新建一个新类包装老逻辑结果绕了一圈代码反而更难懂性能也受了损失。我跟他说开闭原则“对修改关闭”指的是对老模块的“实现”关闭不是对“行为”关闭。对于简单的字段校验这种扩展直接改校验逻辑本身就是合理的抽象过度才是问题。6.3 里氏替换的隐蔽破坏重写父类方法悄悄改了“约定”这个问题比正方形继承长方形隐蔽得多。比如你有个Payment抽象类里面有默认实现public boolean isSuccess()返回true表示支付成功。某个子类因为业务特殊情况在重写时把定义改成了返回true表示“需要人工审核”。接口语义被偷偷换了调用方一懵——这就比setWidth改高度还坑因为它连信号含义都变了。排查这个问题的办法是每当你重写父类方法时问三个问题——父类方法的输入输出约定我是否保持方法行为从调用方视角看是否等价或增强异常范围有没有扩大三个问题里任何一个有“否”就不要用继承改用组合并重新设计接口。6.4 接口隔离和依赖倒置的混淆不少人以为“面向接口编程”就是接口越少越好写一个万能接口包打天下。其实接口隔离是“接口要小而专”依赖倒置是“依赖关系要看抽象层次”两个维度完全不同。我习惯用一个类比接口像插头规格你家里的插座孔位数是固定的不太可能找来一个几十脚的工业插头当万能插头用——接口隔离就是规定每个插头都做自己能承载的那几个脚依赖倒置则是说“电器应该依赖插头规格而不是依赖发电站的内部结构”。6.5 最小知识原则不是“禁止使用getter”有朋友看完LoD之后把自己代码里的user.getAddress().getCity()全部改成在User对象上直接加getCityName()方法理由是“不越过朋友”。这属于矫枉过正。LoD反对的不是跨越多次使用getter而是反对调用链上暴露了对象内部结构关系。当你只有一两次获取时getAddress().getCity()可接受当这条链在十几个地方出现时正确的做法是在领域层提供一个方法封装起来而不是把getCityName()加到User上顺便把封装也做进领域知识里。6.6 实战排查手段用“改动点分析”替代“逐条背诵原则”不做这行的人可能不理解实际排查代码问题最有效的手段不是拿原则逐条对而是做一个“改动点分析”判断未来半年这个模块最可能的变化是什么然后沿着变化方向去看代码结构是否僵化。如果变化方向是“新增支付渠道”代码里却是if-else平铺——OCP破了如果变化方向是“替换存储中间件”高层代码却直接new了具体实现——DIP破了如果变化方向是“多团队并行开发同一模块”接口却很臃肿——ISP破了。 用变化驱动的方式去用这六条原则比死记定义要实用十倍。我给大家一个速查表按“症状-原则-对策”整理症状违反的原则常见对策类膨胀、动一个功能要重新测全部单一职责按变化方向拆分职责新增需求大量修改旧代码开闭原则定义明确的扩展点新增实现类子类重写父类后逻辑对不上里氏替换重审继承关系改用组合接口里很多空实现/抛异常接口隔离按调用方角色拆分接口高层类和低层实现强耦合依赖倒置面向接口编程依赖注入连续get调用链、过度耦合最少知识引入中介/门面封装备援7. 这套方法论还有哪些延伸用法六大原则不只是在代码设计里有用在架构设计、领域建模甚至团队协作里都有延伸。举几个场景做模块划分时微服务的边界设计本质就是在做单一职责和最少知识——每个服务只负责一块清晰的领域能力服务之间通过接口通信调用方不关心服务内部表结构。做接口文档设计时对外API的粒度本质上就是接口隔离——每个调用方只需要它需要的那组能力。做团队分工时模块间的依赖关系设计运用的是依赖倒置——让业务层依赖抽象接口而不是依赖某个人负责的具体实现这样团队并行开发时不会被对方的改动阻塞。所以可以这样说弄懂了六大原则你不只是会写更好的类还会设计更好的系统甚至能更好地划分工作边界。还有一个容易被忽略的场景是遗留系统重构。老系统往往没有好好遵循这些原则当你接到重构任务时就可以用六大原则当路线图先梳理依赖关系找出高层和低层的硬编码依赖DIP再拆上帝类SRP再优化接口边界ISP最后通过隔离变化点OCP把风险最小化。这套顺序我实践过多回比拿着一堆“重构手法”到处乱套有效得多。8. 写在最后的个人体会做开发这些年我最大的体会是六大原则不是背完就行的知识点而是每次写完代码后要回头“照镜子”用的。我到现在写代码还会偶发违反原则的情况区别在于我现在能在code review时一眼看出来并且能跟同事解释清楚“为什么这里该抽象”“为什么那里不该硬塞”。最后分享一个小习惯每次提交代码前我会用自己的模块挑一个类问自己一句——如果下个月产品经理提一个需求要改这个类我需要改几行如果需要动的行数很多或者要改动的是这个类最核心的逻辑说明这个类可能违反了开闭原则或单一职责。这个简单的自查动作帮我提前消灭了无数潜在的重构成本你也可以试试。六大原则的威力不在于每一条多深奥而在于它们组成了一套完整的“代码健康检查表”。如果你现在正在面对一段特别痛苦的老代码不妨对着这个表逐条看一遍很多时候你会发现问题其实早就写在脸上了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑