SOLID 五大原则:面向对象设计的地基
很多人学了二十多个设计模式,却还是写不好代码。原因往往在于:只学了”招式”(模式),没学”心法”(原则)。
SOLID 就是面向对象设计的”心法”,五个字母对应五条原则:
- S:单一职责原则
- O:开闭原则
- L:里氏替换原则
- I:接口隔离原则
- D:依赖倒置原则
理解它们,你才能真正理解那些设计模式为什么那么设计。
S:单一职责原则(SRP)
一个类只应该有一个引起它变化的原因。
说白了:一个类只干一件事。
反例:一个 UserService 既处理用户注册逻辑,又负责发短信、写日志、发邮件。这个类有四个变化原因,任何一处需求变更都要动它。
正例:拆成 UserService、SmsService、EmailService,各司其职。
好处:类的职责清晰,改一个功能不会波及无关逻辑,也更容易测试和复用。
判断标准:如果描述一个类要做的事,需要用”和”字连接(”它负责 A 和 B 和 C”),那大概率违反了单一职责。
O:开闭原则(OCP)
对扩展开放,对修改关闭。
这是五条里最重要的一条,也是所有设计模式的共同目标:加新功能时,尽量通过”新增代码”实现,而不是”修改已有代码”。
为什么?因为修改已有代码有风险——你可能改坏原本正常的功能。
前面的策略模式、工厂方法,都是开闭原则的典型体现:加新策略、新工厂,都是加新类,不用改旧类。
L:里氏替换原则(LSP)
子类必须能够替换父类,且程序行为不变。
通俗说:只要是父类能出现的地方,换成子类也应该没问题。
违反的例子:经典的”正方形继承长方形”。正方形是特殊的矩形(宽高相等),如果让 Square extends Rectangle,重写 setter 让宽高永远相等,就会破坏父类的行为预期——把长方形换成正方形后,setWidth 的行为变了。
这违背了”子类可以无缝替换父类”,说明这个继承关系本身设计错了。
核心警示:继承要符合”is-a”(是一个)的语义,不能为了复用代码而强行继承。
I:接口隔离原则(ISP)
客户端不应该被迫依赖它用不到的接口。
说白了:接口要小而精,不要搞大而全的”胖接口”。
反例:一个 Animal 接口里有 fly()、swim()、run() 三个方法。那么 Dog 实现了这个接口,就被迫实现它根本用不到的 fly() 和 swim()。
正例:拆成 Flyable、Swimmable、Runnable 三个小接口,按需实现。
好处:接口职责单一,实现类只需要实现自己真正关心的方法。
D:依赖倒置原则(DIP)
高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
这是最难理解、也最重要的一条,它其实就是依赖注入的思想来源(下一篇会专门讲)。
反例:
1 | public class OrderService { |
OrderService(高层)直接依赖了 MySQLOrderRepository(低层具体实现)。一旦要换数据库、换存储方式,就得改 OrderService。
正例:
1 | public class OrderService { |
OrderService 只依赖 OrderRepository 接口,具体用 MySQL 还是别的实现,由外部传入。这样高层模块不再被具体实现绑架。
口诀:面向接口编程,而不是面向实现编程。
五条原则串起来看
这五条不是孤立的,它们指向同一个目标——让代码面对变化更从容:
- SRP:一个类一个理由变化 → 变化的影响面小
- OCP:扩展用新增而非修改 → 降低改动风险
- LSP:继承要语义正确 → 避免继承带来的隐性 bug
- ISP:接口小而精 → 减少不必要的耦合
- DIP:依赖抽象不依赖细节 → 高层模块不被底层绑架
而设计模式,就是这五条原则在各种具体场景下的落地招式。
写在最后
我的建议是:先吃透 SOLID,再去看设计模式。
因为模式会忘、会过时,但原则是持久的。真正的高手不是”知道很多模式”,而是”脑子里时刻装着这几条原则,写代码时自然流露”。
下一篇聊开发范式——函数式编程,它是对面向对象思路的一个有益补充。
