SOLID原则实战:五大设计原则重构代码的完整解析 1. 单一职责原则先学会给代码划边界1.1 我接到过的最糟糕的重构任务前几年接手过一个内部报表系统其中一个类摆了快三千行名字叫ReportService。看名字感觉它应该只负责报表生成真正打开文件才发现事情远没有那么简单。它要连数据库查数据、做各种汇总计算、按模板渲染HTML顺便还负责把报表以邮件形式发给指定列表里的人甚至还在类里直接写了记录操作日志的逻辑logger、mailer、excel工具类全部new在方法内部。那个项目当时已经跑了一年多每次提需求都像拆炸弹。有一次产品经理说邮件模板想换一套配色我改完模板渲染方法结果发现发送逻辑里有一处判断依赖旧模板的某个字段直接整条链路报错。还有一次数据库表新增了一个字段按理说只是查询语句加一列的事结果因为解析和渲染耦合在同一个方法里连格式转换都跟着出了偏差。一个看似小小的变更兜兜转转改了一整天。这就是典型的单一职责原则没守住的结果。单一职责原则SRP说的是一个类应该只有一个引起它变化的原因。这句话听起来像哲学翻译成大白话就是你因为什么原因去修改这个类如果邮件服务器换了要改它、报表排版变了也要改它、数据库表结构调整了还要改它那这个类就背负了至少三个不同的职责。职责不是一个动作而是一组高内聚的、会因为同一类需求而变化的行为。把它想成变化的轴线更好理解——每一条导致类需要修改的业务原因就是一条独立的轴线一个类不该同时被多条轴线拉扯。1.2 怎么判断一个类是不是管得太宽很多人问我代码里类分到什么程度才算职责单一有没有量化标准。我的判断方法很粗暴列出这个类所有公开方法然后用一句话描述谁会因为什么原因要求修改它。如果这个类同时要回答数据从哪里来数据长什么样数据发给谁操作有没有留下审计记录这四类问题那基本可以断定它管得太宽了。有个更实用的办法是看测试。给一个职责复杂的类写测试时你会发现要么需要mock一大堆依赖要么一个用例里塞满了完全不相关的断言。比如测试ReportService你得先启动一个假数据库、再配一个假SMTP服务器、还要准备一堆模板文件最后你想验证的其实只是当输入的数字小于零时报表里应该显示红字警告但这个验证被埋在了十几个前置条件后面。测试困难的根源往往是职责过重而不是测试框架不够好。我习惯用角色-依赖法来验证边界先给类定一个明确角色比如报表数据装配器、邮件发送客户端、审计日志写入器然后给每个角色列出它应有的依赖。如果一个类同时依赖数据库连接池、SMTP配置、Excel渲染库和日志SDK那它大概率是多个角色挤在一个文件里。拆分的动作并不复杂把渲染部分挪到HtmlReportRenderer把邮件发送挪到MailDispatcher把日志抽取到独立的AuditLoggerReportService只负责编排顺序。难点不在于拆这个动作而在于你愿不愿意承认当初那个顺手写进去的便利正在变成债务。1.3 单一职责的实操边界不是拆得越碎越好但我也踩过另一个方向的坑。有一段时间我过度痴迷单一职责把一个只有几个方法的业务服务硬拆成七八个类每个类只有一个public方法名字都长到快超过代码行宽。结果是调用方为了完成一件小事要在Controller里手动装配五个对象类之间的依赖图变成一团乱麻同事看了代码直摇头。后来我才意识到单一职责处理的是变化原因的聚合度而不是方法数量的多少。一个类完全可以承载多个相关联的内部方法只要这些方法都是同一个业务目标的组成部分。这里的判断标准是内聚性。一份订单的处理逻辑里校验订单状态计算优惠金额保存订单记录虽然对外暴露了三个方法但它们都属于完成一次下单这个整体职责放在同一个类里完全没问题。真正需要警惕的是把下单和给运营发通知两种变化绑定在一起。通知渠道从邮件换成短信时不应该牵连到订单金额的计算逻辑。所以拆类的时候我会先问自己这两段代码会因为同一个业务决策而变化吗如果不是拆如果是聚着。单一职责落实到团队协作里还有一个隐性作用减少合并冲突。我观察过凡是职责清晰的文件多人并行开发的冲突率明显低。大家各自修改自己负责的类交集小代码评审也更好做。反观那些三五千行的上帝类每次需求变更都挤在同一块区域改冲突、覆盖、改A坏B的惨案几乎必然发生。2. 开闭原则把改代码变成加代码2.1 一个需求变更打回原形的真实案例开闭原则OCP应该是我最早听懂了但最晚做对的一条原则它的核心是软件实体应该对扩展开放对修改关闭。有一回我做一个消息推送模块最初只支持邮件。代码写得很直接一个notify方法里放了一个if判断判断传入的类型是mail就走邮件发送逻辑。后来产品说要多支持短信我加了一段else if再后来要支持App推送我又接了一段else if。每次加新渠道都要动这个中心方法方法越变越长判断条件越摞越多很快就到了连自己都不想再看下去的地步。真正让我意识到问题严重性的是一次排障。生产环境反馈说短信发送一直报错我打开那个满是if/else的通知类排查找了半天发现是之前加App推送时不小心改动了公共的渠道路由变量导致短信组件的配置被提前消费掉了。如果当初每个渠道是独立扩展的类这个bug几乎不可能发生。这就是对修改关闭的意义——你需要一种机制让已有的稳定逻辑不被新功能破坏。开闭原则并不是要求你永远不修改任何代码它说的是你要把那些会发生变化的点用抽象隔离起来让变化通过新增代码来体现而不是通过改写原有核心逻辑来体现。放在消息推送的场景里解法就是定义一个通知渠道接口邮件、短信、App推送各自实现这个接口。当第十个渠道出现时你只需要新增一个类注册进去原来的路由方法一行都不用动。2.2 开闭原则常见的误读我把开闭原则的误读分成两种这两种我都踩过。第一种是完全不允许修改代码。有朋友跟我争论说产品改了需求我必须在现有类里改逻辑难道这违反开闭原则吗其实开闭原则针对的是架构层面的扩展点而不是禁止日常改bug、改文案、改计算规则。如果业务规则本身发生了变化比如优惠门槛从100元改成200元这是内部实现调整不属于扩展范畴直接改没问题。真正需要守住的是接口和扩展点的稳定不是业务数值的永恒不变。第二种误读是提前到处加接口就以为满足了开闭原则。我见过不少代码每个类都配了一个接口接口里方法跟实现类完全一致但没有任何抽象价值。接口不是拿来凑数的装饰品它必须服务一个真正的变化维度。判断标准很简单你是否预见到这个模块会有多个版本或多种策略如果答案是未来可能、但不确定不要急着抽象先用具体类写着等第二个真实变体出现时再抽接口。过早的接口抽象和没有抽象一样糟糕它会增加跳转成本让新人读代码时在一个接口和十几个实现类之间茫然徘徊。真正实用的开闭落地套路我总结下来就三件套识别变化点、定义稳定接口、通过组装而不是改写来扩展。识别变化点是第一步也是最难的一步需要你对业务有一定的预判。定义稳定接口时接口的语义要面向调用方而非面向实现细节。组装方式可以是工厂方法、策略注册表也可以是依赖注入容器但核心思路一致让调用方只依赖抽象让新增实现类通过配置或注册进入系统。2.3 策略模式、模板方法怎么配合开闭原则落地开闭原则是道策略模式、模板方法这些是术。比如前面提到的消息推送用策略模式落地就很自然定义一个Sender接口里面有send(Message)方法MailSender、SmsSender、AppPushSender分别实现。路由的地方维护一个Map把渠道类型映射到对应的Sender实例。新增渠道时写一个新类往Map里加一个键值对完事。这个Map你可以做成配置驱动从数据库或者配置中心读取这样连代码都不用动就能上线新渠道。模板方法则适合那种步骤固定、细节可变的场景。比如一个对账流程整体步骤都是拉取账单、比对差异、生成结果、发送报告但不同渠道的账单格式不同、比对规则不同。这时可以设计一个抽象类把拉取、比对、发送定义为抽象方法把整个流程编排写死在模板方法里。新接入一个渠道继承抽象类填三个方法原来的流程编排完全不动。这个过程就是对扩展开放对修改关闭的直观体现。我自己在项目里最常用的一种开闭配套实践是用注册表代替if/else。把传统的switch-case分发改成注册表查找之后新增一个实现类的成本从读懂整个分发逻辑降为写好一个独立组件并注册。而且这种做法对测试特别友好每个策略可以单独写单元测试不再需要为了触发某个分支而构造一堆前置条件。3. 里氏替换原则继承的正确打开方式3.1 为什么好的继承体系替换无感里氏替换原则LSP可能是SOLID里最容易被忽略、出问题也最隐蔽的一条。它说的是子类必须能够替换掉它的父类并且程序的行为不会因此出现错误。说白了如果你的代码里到处写着如果是子类A就怎么怎么样如果是子类B就怎么怎么样那继承关系大概率是建错了。我印象里最经典的违反案例就是正方形继承长方形。长方形有宽和高两个属性设置宽度会影响面积正方形继承长方形后强制要求宽高相等一旦调用方像操作长方形那样给正方形设置宽高面积计算就全乱了。这个例子虽然简单但在真实项目里每天都在以各种变体发生。比如我碰到过的一个订单系统父类是Order子类叫GroupOrder代表拼团订单。各种业务方法里充斥着if (order instanceof GroupOrder)这样的判断因为父类的实现默认走单人订单流程到了子类这里出现了一堆例外逻辑。好的继承体系应该满足替换无感调用方完全不需要知道当前处理的是哪个子类父类定义的行为契约在子类里全部成立。要达到这个效果关键不在于方法名是否相同而在于行为语义是否一致。子类可以扩展父类没有的方法也可以增强父类已有的能力但不能削弱或改变父类对外承诺的语义。比如父类的save方法保证保存成功后会返回生成的id子类就不能在某些情况下返回null父类承诺getTotalPrice返回含税价格子类就不能偷偷返回不含税的价格。3.2 违反里氏替换的前置条件后遗症违反里氏替换最常见的后遗症就是调用方代码里塞满了类型判断。每次新增一个子类你都要回去翻所有使用父类的地方看看那个if/else要不要再加一个分支。这种代码对扩展极不友好新来的人根本不敢动怕漏掉某个分支导致线上问题。另一个后遗症是方法契约被悄悄破坏父类方法明确要求入参不能为空子类重写时为了图省事没做校验看起来功能正常但当其他调用方按照父类契约传入null时子类直接抛异常最终出问题的是调用方——它明明没有做错任何事。要诊断你的继承体系有没有违反里氏替换可以用一个契约测试的思路把父类的所有测试用例跑在子类上看看有多少用例失败。如果子类复用父类测试套件时大面积失败说明子类并没有真正满足是一个父类的关系。这时该做的不是改测试而是反思继承是否成立。继承在面向对象里是最强的耦合关系一旦建立就很难拆开所以建立继承前一定要问一句这个子类是不是真的在任何父类能用的场景下都能用如果答案里有任何犹豫就考虑用组合代替继承。我自己的团队里定过一条规矩写继承必须同时写契约测试至少要把父类最关键的行为约束用测试固化下来。这么做看起来增加了工作量实际是省了大麻烦。契约测试会在子类破坏语义时第一时间报警而不是等调用方在生产环境里被坑了才回头排查。3.3 实战中怎么识别和收敛继承层次识别继承问题的时机其实很容易当你在代码里写出instanceof的时候或者当你重写父类方法时犹豫要不要调用super的这个方法的时候。如果某个方法的重写逻辑跟父类完全没关系甚至故意跳过了父类的关键校验那就要警惕了这个继承搞不好只是方便复用代码的借口。收敛继承层的实操建议是能组合就不要继承。比如一个通知类它既有邮件发送能力又有短信发送能力与其让SmsNotification继承MailNotification来复用连接池逻辑不如设计一个NotificationSender基类让两个具体发送器都依赖它再把连接池抽取成公共组件通过构造参数传入。这个过程看起来是多写了几个类但每个类的行为边界更清楚测试和替换都更容易。继承只应该发生在语义上是同类且行为契约完全一致的场景比如不同加密算法实现同一个加密算法接口这才是合理的里氏替换关系。还有一个策略是只在层次叶子上用继承把抽象基类设计成不可再随意派生的稳定骨架业务扩展尽量以组合、策略或者装饰器的方式完成。我见过有些项目的继承深度达到五六层每一层都往子类传递几个字段底层子类的构造函数光参数就有十几个。这种代码看似有设计实际上已经失去了设计意义排查问题时每一层都要跳进去看重构成本极高。SOLID好就好在它给了你一个否定的借口只要觉得继承关系可疑你完全可以用里氏替换原则来反驳然后理直气壮地改成组合。4. 接口隔离原则别让调用方为用不到的方法买单4.1 胖接口带来的连锁反应接口隔离原则ISP的核心是客户端不应该被迫依赖它不使用的方法。这句话单独看有点抽象举个例子你就懂了。我做过一个用户服务模块早期为了省事设计了一个UserService接口里面列了十几个方法包括查询用户、更新资料、修改密码、绑定手机、注销账号、导出用户数据、统计用户活跃度等等。结果不同调用方各取所需管理后台要用全部方法用户端App只需要查询和改资料内部运营平台只需要统计和导出。问题很快暴露出来。有一次我调整了导出用户数据方法的返回结构按理说只影响后台导出功能但因为所有调用方都依赖同一个UserService接口编译时所有引用这个接口的地方都要跟着检查一些只需要查询用户信息的模块也被迫进行了代码评审和回归测试。接口越大变更波及面越广这就是胖接口的连锁反应。还有一种更恶心的场景实现类不得不为空方法写空实现。某个类只需要查询方法但因为实现了大接口所有其他方法都得写上一行throw new UnsupportedOperationException。每次看到这种代码我都觉得接口设计者是在给未来埋雷。接口隔离原则的解法很直白把大接口拆成小接口每个小接口对应一组紧密相关的行为。比如把上面那个UserService拆成UserQueryService、UserProfileService、UserAuthService、UserExportService四个接口。根据调用方需求实现类可以实现其中一个、多个或全部。这样用户端模块只需要依赖UserQueryService和UserProfileService改导出服务的接口时它们根本感知不到。重要的是接口隔离不是把类拆开而是把对外的承诺拆开实现类仍然可以是一个完整的类但它对外暴露的视角可以分块。4.2 接口隔离和单一职责的区别很多人容易混淆接口隔离原则和单一职责原则这两个确实长得很像但关注的层面完全不同。单一职责关注的是实现类的内部设计它追问的是一个类是否被多个无关的变化原因拉扯接口隔离关注的是调用方与提供方的契约边界它追问的是一个客户端是否需要感知它用不到的契约。一个类可以有很多方法、承担一个大而完整的职责但如果它把所有方法都放进一个接口里让所有调用方都看到全量方法接口隔离就被破坏了。用一个生活化的类比单一职责是要求一个厨师只负责做菜不要又做菜又收银又洗碗接口隔离则是说顾客进餐厅时不应该看到后厨的所有细节他只需要拿到一张菜单上面列着他能点的菜。厨师可能确实同时负责切菜和炒菜但顾客看到的点单入口接口不应该包含磨刀验收食材这种内部操作。放在代码里就是要把实现者的能力和使用者的视角分开建模。实际工作中接口隔离最常见的落地场景是读写分离。比如一个仓储接口一开始是把save、findById、delete、findAll全部塞在一起。后来我发现很多只读场景根本不需要写方法于是把接口拆成ReadRepository和WriteRepository查询服务只依赖前者管理工具依赖后者。改动的好处立刻体现出来读服务的单元测试可以完全用内存实现的只读仓储不再需要mock一大堆写方法原来实现类为了满足接口而写的空方法也全部消失了。4.3 接缝处的接口该怎么收接口隔离原则还牵扯到一个工程判断接口到底拆到多细才算合适。我的经验是接口的粒度应当跟变化的独立性对齐而不是跟方法的数量对齐。如果几个方法永远一起变、一起被调用那是同一个角色没必要拆开如果某个方法或者某组方法只有特定场景才用那就应该独立成接口。比如订单服务里查询订单详情和取消订单虽然都围绕订单但使用方和变化频率差异很大拆成两个接口是合理的而查询订单详情和按状态查询订单列表经常被同一个页面同时使用留着不拆也完全没问题。另一个需要注意的点是接口隔离不一定是把接口拆小有时候也是给实现类换一个更合适的接口。我曾经接手过一个Socket消息处理模块原来的接口MessageHandler有十一个方法每个实现类都只是零散地用其中三四个。后来我按处理阶段拆成MessageDecoder、MessageValidator、MessageProcessor三个接口实现类根据自己关心的阶段实现对应接口再把各个阶段在编排层串起来。这样每个实现类的代码量没有变少但接口语义清晰了很多新人看代码时能立刻明白一个类在整条链路里到底处于哪个环节。说到底接口隔离是给调用方提供恰好够用的依赖面。它减少了系统的耦合面也减少了测试时要构造的mock数量。很多团队问我要不要引入微服务我的回答通常是先在单体里把接口隔离做好如果你连一个模块内的接口边界都理不清拆服务只会把耦合从方法级放大成网络级问题只会更复杂。5. 依赖倒置原则高层模块的尊严5.1 依赖倒置到底倒置了什么依赖倒置原则DIP说的是两件事高层模块不应该依赖底层模块两者都应该依赖抽象抽象不应该依赖细节细节应该依赖抽象。我第一次读这句话的时候完全没懂依赖倒置四个字到底倒置了什么后来看了一个例子才明白传统分层架构里业务层直接调用数据访问层业务层是高层数据访问层是底层高层依赖于底层。而好的设计应该让业务层定义我需要什么样的数据访问能力这个抽象由数据访问层去实现这个抽象这样依赖的方向变成了底层依赖高层的抽象所以叫倒置。举个例子假设你写了一个报表业务类里面直接new一个MySQLReportRepository。这个类本来应该只关心取数、计算、渲染这个业务流程但现在它被迫知道了数据访问具体来自MySQL未来想换成PostgreSQL或者从缓存里取数都得改业务代码。如果让业务类依赖一个ReportRepository接口接口定义了findReportData这个方法MySQL版、PostgreSQL版、Redis版各自实现业务类只认这个接口依赖就被倒置了。这么做的好处是业务逻辑不再受制于技术选型的波动数据库迁移、缓存引入都变成了一件换实现的事。依赖倒置原则之所以重要是因为高层模块承载的是业务规则和业务流程它们才是系统的核心价值。如果核心价值整天被底层的技术细节牵着走系统就非常脆弱。数据库从一种换到另一种、消息队列从A厂商换成B厂商这些事情不应该导致业务代码大面积改动。业务代码应该像一台稳定运转的机器底层技术是里面的零件零件坏了换个型号就行机器整体不用重造。5.2 依赖注入落地依赖倒置最常用的载体要说依赖倒置在工程上最常见的落地方式那必须是依赖注入DI。依赖注入的核心思想是对象不自己new依赖而是通过构造函数、setter或者方法参数把依赖传进来。这样依赖的绑定关系被放到了对象外部通常由容器或者装配根统一管理。我看到很多刚接触这个概念的人以为用了依赖注入框架就等于满足了依赖倒置原则其实不对。依赖注入只是手段真正要紧的是注入的是抽象如果构造函数注入的是一个具体类那依赖倒置其实没发生。我自己在项目里的习惯是业务模块的构造函数一律接收接口类型装配工作统一放到一个模块初始化方法里。比如一个订单处理器需要记录日志我不会在里面new一个FileLogger而是定义ILogger接口构造函数接收ILogger具体是FileLogger、DbLogger还是KafkaLogger由装配层决定。这样的代码天然可测试单元测试时传一个写内存的假日志器就能验证业务逻辑而不产生任何副作用。依赖注入还有一个隐藏的好处它强迫你把依赖关系显式化。一个类的构造函数参数列表就是它的依赖清单新同事看代码时不需要进到方法体里翻哪里new了什么光看参数就知道这个类依赖哪些外部能力。反过来如果类的方法里散落着一堆new代码的依赖关系就被藏起来了重构和测试都寸步难行。我现在做代码评审第一眼就是看构造函数参数列表和类里有没有new这个习惯帮我发现了大量隐式耦合。5.3 过度依赖抽象的另一个坑但是依赖倒置原则用过头了也有坑最典型的就是万物皆接口。我曾经在一个项目里见过连一个只计算折扣的静态工具类都被套上了接口还专门写了一个Impl类。调用方为了拿到这个折扣计算器要先通过容器注入传给三层最后调一个方法。整个过程抽象的价值为零因为根本没有第二个实现也不会出现第二个实现。这种过度设计不仅增加代码量还让阅读成本成倍上升——每个接口都要跳一层到实现类好几次我调试时都因为跳错实现类浪费了不少时间。我现在的判断标准是什么时候需要抽象当且仅当存在或者可以合理预见到多个实现时。如果只有一个实现直接用具体类把改动留到出现第二个实现时再做。这样的做法在代码里通常用一句话表达YAGNI你不需要它。依赖倒置不是让你把所有细节都抽象化而是让你把稳定的业务规则与容易变化的实现细节分离开。稳定规则是高层的细节是低层的只有当高低层之间存在真实的独立变化需求时倒置才有意义。过度抽象的另一个危害是它会影响性能排查。依赖链变深之后一个简单的业务请求要穿过五六个拦截器、代理、装饰器才能真正到达实现类。线上定位问题时从入口到具体逻辑的距离被拉得很长每层代理都像是在迷雾里加了一层纱。我处理过的性能问题里不少都是因为框架代理层数太多调用链在代理之间来回进出。所以能直连就直连接口们只出现在真正需要面向变化的地方。实用主义在工程里永远是第一位的。6. 六个原则一起用一套真实的判重改造6.1 改造前的代码长什么样把SOLID五条原则单独聊完正好用一套我实际经历过的判重需求改造来做个串联。这个需求是用户注册时系统要根据手机号、邮箱、身份证号、设备指纹等多个维度判断这个用户是否已经存在。最开始的代码把一个正则匹配、一个动态查询、两个第三方风控结果校验全部塞进了同一个方法里整个类四百多行方法内部if嵌套了五层短信验证、数据库查询、外部接口调用直接硬编码在业务方法中连依赖都是直接在方法里new的。这套代码的问题非常典型。首先判重规则如果要从仅精确匹配手机号升级成手机号加设备指纹联合判断必须直接改那个核心方法明显违反开闭原则。其次方法里同时处理了规则解析、数据查询、结果合并、异常包装职责混乱违反单一职责。第三不同判重规则之间有一条隐式的父子关系子规则重写了父规则的比对方式却在很多地方又绕回去调父类的私有方法明显不符合里氏替换。第四调用方无论需要哪个维度的判重都只能依赖这个胖服务接口隔离完全没有。最后业务逻辑直接依赖数据库连接和第三方HTTP客户端依赖倒置约等于零。还有一个致命问题测试几乎没法写。因为所有依赖都是new出来的单元测试想验证手机号重复的用户应该被拦截必须先起一个真实数据库连接再mock掉外部风控接口。跑一套测试的时间足够你去泡杯咖啡回来。需求方后来提出要支持企业用户按统一社会信用代码判重我看着那坨代码知道这已经是最后一次不打碎重构就无法接住的需求了。6.2 分步改造中每个原则的落点改造我分成了四步每一步都对应SOLID中的若干原则。第一步把规则定义从执行逻辑里剥离出来。我定义了一个DuplicateRule接口里面只有一个方法boolean check(DuplicateCheckContext context)。手机号规则、邮箱规则、身份证规则、设备指纹规则各写一个实现类。这一步同时落地了单一职责、开闭和里氏替换三个原则每个类只负责一条规则的判定新规则通过新增类加入所有规则类都遵守同一个输入上下文返回布尔值的语义契约。原本那个四百行大方法的if/else被彻底消灭了。第二步把规则选择机制改成注册表。我用一个DuplicateRuleRegistry来维护规则列表启动时从配置中心读取当前需要启用的规则顺序。调用方只是给我所有启用的规则而不需要关心具体有哪些规则。这就是依赖倒置的雏形上层依赖的是规则集合这个抽象而不是具体某一条规则。同时也让系统的扩展方式从改代码变成了改配置开闭原则在运维层面真正落了地。第三步把判重结果的处理和通知拆出去。原来判重方法里还夹着如果发现重复就给运营发邮件的代码我把这个逻辑挪到了DuplicateResultHandler里它接收判重结果、决定要不要通知、通过什么渠道通知。这样判重核心流程不再关心邮件还是短信通知渠道的变化被隔离在一个独立接口后面。这也是接口隔离的体现判重的调用方不需要知道结果会去哪、怎么通知它只关心判重结果本身。第四步把数据库查询和外部风控调用都抽象成接口。DuplicateCheckContext需要的数据不再由判重方法自己去查而是通过一个DataProvider接口传入。具体是查MySQL、Redis还是调第三方风控API都隐藏在实现类后面。判重规则本身只依赖上下文对象对数据的来源一无所知。改造完成后单元测试直接构造一个内存版DataProvider传几个测试数据就能跑完整个判重流程测试速度从分钟级降到了毫秒级。6.3 重构后的效果和我的启发改造完之后的代码结构发生了质的变化。核心判重类从四百多行瘦身到六十多行主要工作是遍历启用规则、汇总结果、交给结果处理器。任何一条新判重规则从需求提出到上线只需要写一个新的规则类、在配置中心开启开关连测试都不用碰其他规则。企业用户按统一社会信用代码判重的需求我在两个小时内完成开发并提交测试这在改造前是不可想象的——光是理清旧代码里那些交错的分支就要花半天。测试的改善同样明显。每个规则类有独立的单元测试注册表有注册顺序测试结果处理有通知渠道测试。整个模块的测试覆盖率从原来的不到百分之二十提升到百分之八十五左右。代码评审也轻松了很多新增规则类的评审只需要关注一个文件而不需要理解整条链路。有一回一个新同事加入团队看这套代码只花了半天就理清了判重的完整流程放在以前一个月可能都摸不清边界。聊了这么多如果说我对SOLID有什么新的体会那就是这五个原则从来不是独立的教条而是一套互相咬合的设计观。单一职责帮你找到变化的边界开闭原则帮你把边界变成扩展点里氏替换保证继承不乱来接口隔离让依赖关系清爽依赖倒置让架构重心回到业务本身。如果你在实践中觉得某个原则不好用多半不是原则错了而是你还没找到那个真正让它发挥作用的切入点。代码写久了自然会有手感但有了SOLID这套尺子你的手感会更有底气。这大概就是设计模式原则对普通开发者最大的价值——它让你在说这段代码设计得不好的时候能说出具体哪里不好、以及往哪个方向改。