设计模式的开门砖 - 六大设计原则 - 开闭原则 - C#
在学习设计模式之前,我强烈建议你先搞懂设计原则。
前言:为什么要先学设计原则?
用最通俗的话来解释:
-
设计原则是“指导思想”——告诉你什么方向是对的
-
设计模式是“通用模板”——告诉你针对某个具体问题怎么写代码是对的
设计模式就是设计原则的“现成答案”。
如果你不了解原则,直接学模式,很容易出现几种情况:
-
为了用模式而用模式,写出又重又绕的代码
-
换个场景就不会用了,因为只记得“怎么写”,不明白“为什么这么写”
当你先明白了原则,再看策略模式、观察者模式等,你就会恍然大悟:
“原来这个模式就是为了实现开闭原则而总结出来的一套经典写法。”
原则是道,模式是术。先悟道,再学术,事半功倍。
一、设计原则是什么?
词语定义:
设计原则是前辈们在长期的软件开发实践中总结出来的通用指导原则,用来帮助我们写出更容易维护、扩展、复用和理解的代码。
它不是具体的代码(不是库、不是框架),也不是外面的语法规则(不遵守代码也能跑),而是一种“最佳实践”或“编程好习惯”。
设计原则解决了什么问题?
无设计原则时常见现象:
-
改一个功能,拆东墙补西墙
-
加一个小需求,改十几种
-
一种几千行,谁也不敢动
设计原则的目标就是避免上面这些情况。
设计原则的本质
识别变化 + 隔离变化
好代码不是一成不变的,而是把容易变化的部分和稳定的部分分开,这样需求增加,你只需要改变“变化区”,不碰“稳定区”。
六大设计原则草案速览
| 原则 | 核心一句 |
|---|---|
| 开闭原则 | 增加功能多加新类,少改旧类 |
| 里氏替换原则 | 子类要能替换父类不出错 |
| 依赖倒置原则 | 依赖接口,不依赖具体类 |
| 接口隔离原则 | 接口别太胖,深入定制 |
| 单一职责原则 | 一类只干一件事 |
| 迪米特原则 | 别和陌生人说话 |
二、开闭原则(OCP)的概念
标准定义
Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
—— 伊恩·霍兰德(Ian Holland),1987年
中文翻译:
一个软件实体(类、模块、函数等)应该对开放进行扩展,对修改关闭。
两个词拆解
| 要点 | 意义 | 通俗解释 |
|---|---|---|
| 对扩展开放 | 当需求变化时,可以通过添加新代码来改变系统行为 | 添加新功能时,你写新类、新方法,而不是改旧的 |
| 对修改关闭 | 已有的代码(尤其是核心模块)尽量不要去序列 | 已经测试通过、稳定运行的代码,最好别再碰它 |
通俗理解
开闭原则通俗讲就是:在程序设计之初,你就应该先准备好扩展的方法,保证后续在需求变更时,可以继续或少动之前的源代码。
话语记住
加功能,加新类;别动不动就去改旧类。
三、违反 vs 符合开闭原则(C#代码对比)
违反开闭原则
// 违反开闭原则:每增加一种形状,都要修改AreaCalculator类 public class AreaCalculator { public double Calculate(object shape) { if (shape is Rectangle rect) { return rect.Width * rect.Height; } else if (shape is Circle circle) { return circle.Radius * circle.Radius * Math.PI; } //新增三角形需求 → 必须修改这个方法,加新的else if return 0; } } public class Rectangle { public double Width { get; set; } public double Height { get; set; } } public class Circle { public double Radius { get; set; } }问题分析:
每修改增加一种新形状,都要
Calculate方法违反 “对修改关闭” 原则
修改稳定代码容易引入新的Bug
符合开闭原则
// 1. 抽象接口:对扩展开放 public interface IShape { double Area(); } // 2. 具体实现类:各形状独立实现 public class Rectangle : IShape { public double Width { get; set; } public double Height { get; set; } public double Area() { return Width * Height; } } public class Circle : IShape { public double Radius { get; set; } public double Area() { return Radius * Radius * Math.PI; } } // 3. 稳定模块:对修改关闭 public class AreaCalculator { public double Calculate(IShape shape) { // 核心:依赖抽象接口,永远不需要修改这个方法 return shape.Area(); } }优势分析:
新增三角形需求→添加
Triangle类,实现IShape接口
AreaCalculator.Calculate方法一行代码都没有用改符合 “对扩展开放,对修改关闭”
扩展示例:增加三角形
// 新增形状:完全不影响已有代码 public class Triangle : IShape { public double Base { get; set; } public double Height { get; set; } public double Area() { return Base * Height / 2; } } // 使用示例 var calculator = new AreaCalculator(); double area = calculator.Calculate(new Triangle { Base = 10, Height = 5 });
对比总结表
| 违反开闭原则 | 符合开闭原则 | |
|---|---|---|
| 实现方式 | 大量 if/else 或 switch |
接口+多态 |
| 加新形状时 | 修改 AreaCalculator 类 |
新增 Triangle 类 |
| 修改已有代码 | ✅ 修改需要 | ❌ 不需要修改 |
| 稳定性 | 低,每次都可能引入Bug | 高,稳定模块被隔离保护 |
| 可维护性 | 差,类日益增加 | 好,每类职责单一 |
总结
使用
if/else判断类型→违反开闭原则
用接口+多态→符合开闭原则
四、开闭原则的本质
开闭原则的本质 = 抽象 + 多态 + 面向接口编程
-
Abstract(接口/父类):定义了稳定的契约,不轻易改变
-
多态(具体实现):变化的部分被封装在子类中,可以自由扩展
-
面向接口编程:依赖接口具体而不是实现,让扩展成为可能
把稳定和变化分开,稳定部分抽象,变化部分具体实现。
五、如何实现开闭原则?
| 手段 | 说明 |
|---|---|
| 接口/抽象类 | 定义稳定的方法签名,实现类可以随意增加 |
| 联合继承 | 通过注入不同的策略对象来改变行为,而不是改变父类 |
| 设计模式 | 策略模式、模板方法模式、观察者模式等,都是开闭原则的经典落地 |
六、常见误区
| 误区 | 正解 |
|---|---|
| “不能修改任何代码” | 不是不改,而是核心模块/已经稳定的模块尽量不改。刚开始设计时可以完全调整 |
| “把所有东西都抽象” | 过度设计。只对可能变化的部分做抽象,不要提前过度泛化 |
| “只要增加新类就能解决所有问题” | 确实有时需要修改老代码(比如修复bug),这并不违反开闭原则。原则针对的是功能扩展 |
七、结语:写得少 vs 写得多
你可能听过这样一句话:“能用最少的代码把需求实现出来的程序员,才是高手。”
这确实是。但学了开闭原则之后,你可能会产生一个疑问:
设计原则显然让我写接口、写抽象、做扩展设计,代码量反而变多了。这不是矛盾吗?
答案是:不矛盾,只是观察“代码量”的时间测量不同。
短视角(只看今天)
| 违反开闭原则 | 符合开闭原则 | |
|---|---|---|
| 今天写代码量 | 少(直接写if/else,一把梭) | 多(定义接口、做抽象、拆分结构) |
| 今天的样子 | ✅ 高效 | ❌啰嗦,过度设计 |
纵向视角(看三个月、一年后)
| 违反开闭原则 | 符合开闭原则 | |
|---|---|---|
| 第5次需求变更 | 一定要改掉老代码,类越来越多的营销事件 | 加新类就行,老代码纹丝不动 |
| 累计代码变化量 | 改大量老代码 | 只加了新代码 |
| 引入Bug的风险 | 高(改一处可能炸多处) | 低(新代码不影响老逻辑) |
| 维护成本 | 指数级上升 | 线性生长 |
一句话破
“用最少的代码实现需求”说的是:不要写无用、重复的代码。
设计原则要求你“多写”的是:为未来的变化装备空间的代码,是隔离变化的第三个,是让整个系统能够活过第二次、次需求变更的代码。
两者不冲突:
-
无原则的少写 → 今天爽,三个月后想重构
-
有原则的多写 →今天多花10分钟,半年后少花10个小时
真正的厉害
厉害的程序员不是在今天用最少的代码把需求糊出来。
而是知道哪些代码可以少写,哪些代码必须多写,并且能准确判断这个“度”。
开闭原则给了你一个判断标准:
-
稳定的核心逻辑 → 值得抽象,值得多写几行接口
-
频繁变化的部分 → 隔离出来,别跟稳定代码搅在一起
金句收尾
代码写得少,不一定是高手;
但能让半年后的同事(包括你自己)少掉头发,一定是高手。开闭原则,就是那把护发神器。
下一篇预告:迪米特法则 - 宝宝乖!不要和陌生人说话!
如果觉得文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我更新的动力!
更多推荐
所有评论(0)