🎯 导读:本文将用生动有趣的方式,带你深入理解面向对象设计的四大核心原则。每个原则都配有 🎭 生活比喻、💻 完整代码案例 和 📊 UML类图,让你轻松掌握这些看似高深的设计思想!
🏛️ 前言:为什么需要设计原则?
想象一下,你正在搭建一座大厦:
- ❌ 没有原则:就像用泥巴和树枝随意堆砌,风一吹就倒
- ✅ 遵循原则:就像使用钢筋混凝土的标准结构,稳固且可扩展
设计原则就是软件世界的”建筑规范”,它们让我们的代码:
- 🧩 易于维护 —— 修改一处,不影响全局
- 🚀 易于扩展 —— 新增功能,不用改老代码
- 🤝 易于协作 —— 团队成员都能快速理解
📐 原则一:单一职责原则 (SRP)
“一个类应该只有一个引起它变化的原因”
🎭 生动比喻:瑞士军刀 vs 专业厨房
| 场景 | 形象 | 问题 |
|---|---|---|
| ❌ 违反SRP | 🔪 瑞士军刀 | 既能切菜、开红酒、拧螺丝、剪指甲… 看似全能,但哪个功能都不专业!剪刀坏了,可能影响到拔塞器 |
| ✅ 遵守SRP | 👨🍳 专业厨房 | 切肉有斩骨刀,削皮有削皮刀,开红酒有海马刀。各司其职,哪把钝了磨哪把 |
📊 UML 对比
❌ 违反 SRP —— “上帝类” 设计
classDiagram
class OptimizationReport {
- string reportData
+ processData()
+ formatToHTML()
+ saveToFile()
}
🔥 问题:一个类干了3个人的活!任何需求变更都要修改这个类,高耦合、高风险
✅ 遵守 SRP —— 各司其职
classDiagram
class OptimizationData {
- double maxStress
- double minDensity
+ getRawData()
}
class ReportFormatter {
+ formatAsHTML()
+ formatAsMarkdown()
}
class DataSaver {
+ saveToDisk()
+ saveToCloud()
}
OptimizationData ..> ReportFormatter : 被格式化
ReportFormatter ..> DataSaver : 被保存
✅ 单一职责:每个类只负责一件事
💻 代码实战
❌ 反面教材:大包大揽的”上帝类”
1 |
|
⚠️ 为什么说这个设计很糟糕? 因为它有 **3个”引起它变化的原因”**:
- 📊 改变数据处理逻辑(增加应变数据)→ 要改这个类
- 🎨 报告格式从 HTML 改成 PDF → 要改这个类
- 💾 存储从本地改成云端 → 还要改这个类
🔥 一个类被修改的理由太多 = 高耦合 = 牵一发而动全身!
✅ 正面教材:拆分解耦,各司其职
1 |
|
🎯 SRP 核心总结
| 好处 | 说明 |
|---|---|
| 🧩 代码像积木 | ReportFormatter 可被系统任何地方复用 |
| 🛡️ 修改不胆战心惊 | DataSaver 出 BUG 不会影响数据计算逻辑 |
| 👥 团队协作顺畅 | 三个程序员可同时开发,无代码冲突 |
💡 判断技巧:试着用一句话描述类的功能。如果用到”和“、”并且“、”此外“,大概率违反了 SRP!
🔓 原则二:开放封闭原则 (OCP)
“对扩展开放,对修改封闭”
- ✅ 对扩展开放:需求变化时,能够方便地增加新功能
- 🔒 对修改封闭:增加新功能时,不修改已测试通过的老代码
🎭 生动比喻:电脑的 USB 接口 🖥️🔌
| 特性 | 说明 |
|---|---|
| ✅ 对扩展开放 | 买新鼠标、键盘、USB风扇,都能直接插上使用 |
| 🔒 对修改封闭 | 不需要拆开机箱,把鼠标线焊接到主板上! |
核心思想:通过预留标准接口,让新功能像插件一样”插”进系统!
📊 UML 对比
❌ 违反 OCP —— 被 if-else 支配的恐惧
classDiagram
class PaymentProcessor {
+ processPayment()
}
❌ 问题:新增支付方式必须修改核心类,容易引入 BUG,违反”对修改封闭”
代码示例:
1 | if (payType == 'Alipay') { ... } |
✅ 遵守 OCP —— 面向接口,热插拔
classDiagram
class IPaymentMethod {
+ pay()
}
class PaymentProcessor {
+ processPayment()
}
class AliPay {
+ pay()
}
class WeChatPay {
+ pay()
}
class ApplePay {
+ pay()
}
class UnionPay {
+ pay()
}
PaymentProcessor ..> IPaymentMethod : 依赖接口
AliPay ..|> IPaymentMethod : 实现
WeChatPay ..|> IPaymentMethod : 实现
ApplePay ..|> IPaymentMethod : 实现
UnionPay ..|> IPaymentMethod : 实现
✅ 完美遵守 OCP:这个方法永远不需要再修改!只依赖抽象接口,不依赖具体实现
🆕 新增支付方式:只需添加新类,无需修改原有代码
💻 代码实战
❌ 反面教材:if-else 地狱
1 |
|
⚠️ 噩梦场景:公司业务拓展,要求接入 Apple Pay 和 银联支付。
你被迫打开
PaymentProcessor核心类,继续增加else if。🔥 风险:加 Apple Pay 时不小心删了支付宝的逻辑 → 整个支付系统崩溃!
✅ 正面教材:策略模式 + 多态
1 |
|
🆕 见证奇迹:扩展新功能无需修改老代码!
1 | // 🎉 只需要新增代码,绝不修改老代码!(对扩展开放) |
🎯 OCP 核心总结
| 问题 | 解决方案 |
|---|---|
| 🤔 为什么需要 OCP? | 软件需求变化是绝对的。如果每次变动都要修改底层核心代码,系统会越来越脆弱(代码腐化) |
| 🛠️ 怎么实现 OCP? | 秘诀是 **”抽象”**(接口/抽象类)+ **”多态”**。把易变部分抽象成接口,调用方只依赖接口 |
| 🎨 和设计模式的关系 | 策略模式就是 OCP 最完美的践行者!OCP 是思想,策略模式是具体战术 |
🔄 原则三:依赖倒置原则 (DIP)
“高层模块不应该依赖低层模块,两者都应该依赖抽象”
🎭 生动比喻:墙壁插座与家用电器 🔌
| 角色 | 类比 |
|---|---|
| 🏠 高层模块 | 你家房子的供电系统 |
| 📺 低层模块 | 电视机、电冰箱、电灯泡 |
❌ 违反 DIP(硬连接)
装修工人把供电系统的电线,直接焊死在电视机内部。
- 🔥 后果:电视机坏了想换电冰箱?得把墙砸了,重新焊接!
✅ 遵守 DIP(引入插座)
国家规定 “三孔插座标准”(抽象接口):
- 🔌 供电系统(高层):只负责把电送到插座,不关心外面接什么
- 🔌 电视机(低层):只负责设计能插进插座的插头
- ✅ 结果:想插什么电器就插什么,墙壁永远不需要改动!
📊 UML 对比
❌ 违反 DIP —— 高层直接依赖低层
classDiagram
class TopologyProject {
- LocalDiskWriter diskWriter
+ saveResult()
}
class LocalDiskWriter {
+ writeToDisk()
}
TopologyProject --> LocalDiskWriter
❌ 致命错误:高层模块直接绑定低层模块,想换云端存储?必须剖开核心代码!
✅ 遵守 DIP —— 依赖注入
classDiagram
class IStorage {
+ saveData()
}
class TopologyProject {
- IStorage storage
+ TopologyProject()
+ saveResult()
}
class LocalDiskWriter {
+ saveData()
}
class CloudStorageWriter {
+ saveData()
}
TopologyProject ..> IStorage : 依赖抽象
LocalDiskWriter ..|> IStorage : 实现
CloudStorageWriter ..|> IStorage : 实现
✅ 依赖注入:通过构造函数传入具体实现,高层只认接口,不认具体类
🆕 新增云端存储:无需修改 TopologyProject 一行代码!
💻 代码实战
❌ 反面教材:高层直接依赖低层
1 |
|
⚠️ 痛点:公司要求软件支持”保存到阿里云数据库”。
你必须把
TopologyProject(核心高层代码)剖开,把LocalDiskWriter删掉换成CloudWriter。🔥 极易引入 BUG!
✅ 正面教材:依赖注入大法
1 |
|
🎯 DIP 核心总结
为什么叫”倒置”(Inversion)?
1 | 传统思维(自顶向下): |
四大原则的关系
| 原则 | 作用 | 关系 |
|---|---|---|
| 📐 SRP | 让类保持小巧和纯洁 | 基础:扎马步 |
| 🔓 OCP | 最终目标:不改老代码,只加新代码 | 目标:武功大成 |
| 🔄 DIP | 实现 OCP 的最核心手段 | 心法:内功 |
| 🎨 策略/工厂模式 | 落地到代码的具体招式 | 招式:降龙十八掌 |
💡 一句话总结:DIP 就是通过面向接口编程,让高层和低层都依赖于抽象,而不是相互直接依赖!
🧬 原则四:里氏替换原则 (LSP)
“所有引用基类的地方必须能透明地使用其子类的对象”
由计算机科学家 芭芭拉·里斯科夫(Barbara Liskov) 提出,专门约束继承关系。
🎭 生动比喻:老爹的炸鸡店 🍗
你老爹开了一家”老爹炸鸡店”(父类),契约是:”本店提供美味的炸鸡“。
❌ 违反 LSP —— 坑爹的儿子
你(子类)继承了店,但为了省钱,把炸鸡换成了炸馒头,还说:”都是炸的,差不多!”
🔥 后果:老顾客(客户端代码)来点餐,发现味道完全不对,程序崩溃!
✅ 遵守 LSP —— 合格的继承者
你(子类)继承了店,提供了更美味的炸鸡(优化实现),但契约不变。
✅ 结果:老顾客完全无感知,甚至觉得更好吃了!
📊 UML 对比
❌ 违反 LSP —— 子类破坏父类契约
classDiagram
class Bird {
+ fly()
}
class Sparrow {
+ fly()
}
class Ostrich {
+ fly()
}
Bird <|-- Sparrow : 继承
Bird <|-- Ostrich : 继承
❌ 违反 LSP:鸵鸟重写了 fly() 方法,但里面写着 throw Exception(“不会飞”) 或空的 {} 来掩盖错误。用 Bird 的地方替换为 Ostrich 会崩溃!
✅ 遵守 LSP —— 正确的继承层次
classDiagram
class Animal {
+ move()
}
class Bird {
+ fly()
}
class Sparrow {
+ fly()
}
class Ostrich {
+ run()
}
Animal <|-- Bird : 继承
Animal <|-- Ostrich : 继承
Bird <|-- Sparrow : 继承
✅ 遵守 LSP:鸵鸟不继承 Bird,而是直接继承 Animal,实现自己的 move()。不会飞的鸟就不应该继承会飞的鸟!
💻 代码实战
❌ 反面教材:鸵鸟继承鸟(经典错误)
1 |
|
⚠️ 问题分析:
- 🦩 鸵鸟是鸟,但不会飞
- 🔥 强迫鸵鸟继承
Bird类,违反了自然属性- 💥 用
Bird*的地方替换为Ostrich*,程序行为异常!
✅ 正面教材:正确的继承层次设计
1 |
|
🎯 LSP 核心总结
判断标准
| 检查项 | 说明 |
|---|---|
| ✅ 行为一致 | 子类替换父类后,程序行为不变 |
| ✅ 契约遵守 | 子类不破坏父类的前置条件、后置条件 |
| ❌ 反模式 | 子类重写方法时抛出异常或空实现来掩盖错误 |
常见违反场景
1 | // ❌ 危险信号:这些写法 100% 违反 LSP! |
解决方案
如果发现子类需要”破坏”父类的行为,说明继承关系设计错误:
- 🔄 抽离更细的接口 —— 如
Flyable和Runnable分开 - 🔀 改为组合关系 —— 用”有一个”代替”是一个”
💡 一句话总结:子类必须能够 100% 替代父类,而不改变程序的正确性!
🎓 总结:四大原则的关系
1 | ┌─────────────────────────────────────────────────────────────┐ |
🎯 记忆口诀
“单开依里” —— 单(SRP)开(OCP)依(DIP)里(LSP)
记住:单身开放,依然美丽 😄
📝 本文通过 🎭 生活比喻、📊 UML类图、💻 完整代码案例,帮助你深入理解设计模式四大原则。希望对你有所帮助!
- 本文作者: 迪丽惹Bug
- 本文链接: https://lyroom.github.io/2026/04/27/设计模式的四大原则/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!