乐于分享
好东西不私藏

SOLID 原则(软件开发实践)

SOLID 原则(软件开发实践)

SOLID 原则(软件开发实践)

SOLID 是面向对象设计中最常用的一组设计原则,用来提升代码的可维护性、可扩展性、可测试性
它不是为了“炫技”,而是为了让系统在需求变化时更容易演进。


1. S 单一职责原则(Single Responsibility Principle, SRP)

原则说明

一个类(或模块)应该只有一个引起它变化的原因。
换句话说:一个类只做一件事,并把这件事做好。

反例场景

在电商系统中,一个 OrderService 同时负责:

  • 订单创建
  • 库存扣减
  • 支付调用
  • 发短信通知
  • 生成报表

当短信渠道改版时,你需要修改 OrderService
当报表规则变更时,也要修改 OrderService
这个类会频繁被不同原因改动,风险大、测试成本高。

正例场景(拆分职责)

将职责拆分为多个独立服务:

  • OrderService:只负责订单流程编排
  • InventoryService:负责库存逻辑
  • PaymentService:负责支付逻辑
  • NotificationService:负责通知发送
  • ReportService:负责报表生成

好处:

  • 改动影响面更小
  • 单元测试更聚焦
  • 团队协作时冲突更少

2. O 开闭原则(Open-Closed Principle, OCP)

原则说明

软件实体应该对扩展开放、对修改关闭
新增能力时优先通过扩展实现,而不是反复修改已有稳定代码。

反例场景

支付模块最初只支持支付宝和微信,代码如下思路:

  • if (type == "ALIPAY") ...
  • else if (type == "WECHAT") ...

后来要加银联、PayPal、数字钱包,每次都修改同一个大 if-else
结果是:老逻辑容易被误伤,回归测试范围越来越大。

正例场景(策略扩展)

定义统一支付接口 PaymentMethod,不同支付方式各自实现:

  • AlipayPayment
  • WeChatPayment
  • UnionPayPayment
  • PayPalPayment

主流程只依赖接口,通过注册表或工厂选择具体实现。
新增支付方式时只需新增实现类并注册,无需改动核心流程。

好处:

  • 新功能接入成本低
  • 稳定模块不被频繁改动
  • 更符合插件化思路

3. L 里氏替换原则(Liskov Substitution Principle, LSP)

原则说明

子类必须能够替换父类,并且不改变程序原有正确性。
即:使用父类的地方,换成任意子类后行为应保持一致预期。

反例场景

设计了 Bird 父类,包含 fly() 方法。
Sparrow(麻雀)继承没问题,但 Penguin(企鹅)也继承后无法飞,只能在 fly() 里抛异常。

如果业务代码把所有 Bird 都当成“可飞行”,替换为 Penguin 就会运行失败,违反 LSP。

正例场景(抽象层次重构)

将能力拆分:

  • Bird:只表达“鸟类”通用属性
  • Flyable:可飞行能力接口
  • SparrowBird + Flyable
  • Penguin:仅 Bird

依赖“飞行能力”的逻辑只接收 Flyable
这样替换关系符合语义,不会出现“表面继承、行为违约”。

好处:

  • 继承关系更真实
  • 多态行为更可靠
  • 运行时异常更少

4. I 接口隔离原则(Interface Segregation Principle, ISP)

原则说明

不应该强迫客户端依赖它不需要的方法。
接口应“小而专”,按角色拆分,而不是做“胖接口”。

反例场景

定义一个通用设备接口 IMachine

  • print()
  • scan()
  • fax()

普通打印机只支持打印,却被迫实现扫描和传真(通常是空实现或抛异常)。
这会制造无意义代码和隐藏故障点。

正例场景(按能力拆接口)

拆成多个小接口:

  • IPrinter
  • IScanner
  • IFax

设备按需实现:

  • 普通打印机实现 IPrinter
  • 一体机实现 IPrinter + IScanner + IFax

好处:

  • 实现类更干净
  • 调用方依赖更精确
  • 变更影响更可控

5. D 依赖倒置原则(Dependency Inversion Principle, DIP)

原则说明

高层模块不应依赖低层模块,二者都应依赖抽象;
抽象不应依赖细节,细节应依赖抽象。

反例场景

用户注册流程 UserService 内部直接 new MySqlUserRepository()
后续如果要改为 PostgreSQL、MongoDB 或远程 API,需要改 UserService 源码并连带改测试。

正例场景(面向抽象编程)

让 UserService 依赖 UserRepository 接口,由外部注入具体实现:

  • MySqlUserRepository
  • PostgresUserRepository
  • MockUserRepository(测试用)

这样高层业务不关心数据存储细节,替换实现不影响核心业务代码。

好处:

  • 业务层与基础设施解耦
  • 更利于测试(可注入 Mock)
  • 技术栈替换成本更低

实战落地建议

  • 从“变化点”入手:哪里最常改,哪里就最需要 SOLID
  • 先小步重构:不必一次到位,可从新增需求处渐进改造
  • 避免过度设计:简单场景优先可读性,复杂增长后再抽象
  • 结合测试推进:重构时配套单元测试,降低回归风险

总结

SOLID 的核心目标不是增加类和接口数量,而是降低变化成本。
当需求持续变化时,遵循 SOLID 的系统通常更容易扩展、更少牵一发动全身,也更适合多人协作开发。