写在前面
我写了 DDD 系列,里面反复出现"策略"“工厂"“观察者"这些词——它们都是设计模式。这篇开一个新坑:「设计模式实战(.NET 视角)」。
市面上讲 23 个 GoF 模式的文章海了去了,这个系列的差异化有三点:用 现代 C#(Func/record/模式匹配)写,而不是 1998 年的 Java 式类层次;用 ASP.NET Core / .NET 源码做案例;回链你已有的内容(SOLID 接整洁架构、责任链接中间件、观察者接领域事件…)。
第一篇不急着讲具体模式,先把地基打好:模式到底是什么,以及它们共同的底座——SOLID。
一、模式到底是什么
1
2
3
4
5
6
7
8
9
10
| 常见误解:模式是"必须照着写的标准答案"
真相: 模式是"已被验证的、有名字的设计经验"
- 它首先是"词汇":你说"这里用策略模式",对方秒懂你在说什么
→ 沟通效率 >> 代码本身
- 其次是"经验浓缩":前人踩过的坑、试过的解法,给它起个名
- 它有"适用场景"和"代价",不是万能解
类比:模式之于编程,像"套路"之于棋手。
背套路不是为了死套,是为了不在低级地方浪费时间。
|
1
2
3
4
5
6
7
| 学模式的三个层次:
1. 知道有这个名字(能听懂别人说)
2. 会写出来(能照着实现)
3. 知道什么时候用、什么时候不用(真正会用)← 目标
最高境界:忘了模式名,但写出来的代码天然符合模式。
因为好设计的归宿就那几种,殊途同归。
|
二、模式的代价(别滥用)
1
2
3
4
5
6
7
8
9
10
| 模式不是免费的:
✗ 引入抽象 → 代码变多、间接层变多
✗ 增加理解成本(读代码的人要认识这个模式)
✗ 用错地方 → 过度设计,比没用更糟
铁律:模式应对的是"真实存在、反复出现的复杂度",
而不是"想象中、将来可能"的复杂度(YAGNI)。
一次 if/switch 能解决的事,别上策略模式。
等真的"第三个算法"出现、switch 开始膨胀,再重构不迟。
|
三、SOLID:所有模式的底座
23 个 GoF 模式,骨子里都在贯彻五条原则——SOLID。理解了 SOLID,模式就是它的具体应用。
1
2
3
4
5
| S — Single Responsibility Principle 单一职责
O — Open/Closed Principle 开闭原则
L — Liskov Substitution Principle 里氏替换
I — Interface Segregation Principle 接口隔离
D — Dependency Inversion Principle 依赖倒置
|
3.1 单一职责(SRP)
1
2
3
4
5
6
7
8
| 一个类/函数,只有一个变化的理由。
反例:一个 OrderService 又算价格、又发邮件、又写日志、又管权限
→ 任何一个需求变化(改邮件模板、改权限规则)都要动这个类
→ 改一处怕牵连,测试爆炸
正解:拆。算价归 PricingService,发邮件归 Notifier,各管一摊。
"变化的理由" = "谁会来要求改它"。
|
3.2 开闭原则(OCP)
1
2
3
4
5
6
7
8
9
| 对扩展开放,对修改关闭。
→ 加新功能时,加新代码,不改老代码。
反例:折扣逻辑写在一串 if/switch 里
新增"满减"活动要往 switch 里塞分支 → 改老代码(有回归风险)
正解:抽象出 IDiscount,每种折扣一个实现
新增满减 = 加一个类,老代码一行不动
(这就是策略模式,下一篇讲)
|
3.3 里氏替换(LSP)
1
2
3
4
5
6
7
8
9
| 子类必须能无感替换父类,不破坏程序正确性。
经典反例:正方形继承长方形
Rectangle.SetWidth(5).SetHeight(3).Area() == 15
Square 重写 SetWidth 同时改高 → Area != 15 → 行为变了
规矩:子类可以"更强",但不能"违约"。
覆写方法不能抛父类没声明的异常、不能改预期能接受的入参范围。
"is-a" 关系一旦在行为上不成立,继承就是错的。
|
3.4 接口隔离(ISP)
1
2
3
4
5
6
7
8
| 不要强迫调用方依赖它用不到的方法。
→ 接口要小而专,不要胖接口。
反例:一个 IBird 既有 Fly() 又有 Walk()
企鹅实现 IBird 就得写个抛异常的 Fly() → 别扭
正解:拆成 IBird(Walk)、IFlyingBird(Fly),各取所需
→ 客户端只依赖它真正用的接口
|
3.5 依赖倒置(DIP)—— 最重要的一条
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 高层模块不该依赖低层模块,二者都该依赖抽象。
抽象不依赖细节,细节依赖抽象。
→ "依赖方向永远指向抽象"
反例:OrderController 直接 new SqlOrderRepository()
→ Controller 依赖了具体的 SQL 实现
→ 换 Mongo、换内存测试,都要改 Controller
正解:OrderController 依赖 IOrderRepository(抽象)
具体的 SqlOrderRepository 实现 IOrderRepository
由 DI 容器注入
这就是 DDD 第六篇「整洁架构」的核心——
领域层定义接口(抽象),基础设施层实现,依赖方向向内。
SOLID 的 D,几乎是现代框架(ASP.NET Core DI)的立身之本。
|
四、SOLID 一张图串起来
1
2
3
4
5
6
7
8
9
| SRP:一个类一个变化理由 → 控制"改起来怕牵连"
OCP:加功能不改老代码 → 控制"扩展的代价"
LSP:子类不违约 → 控制"继承的安全性"
ISP:接口要小 → 控制"依赖的纯洁度"
DIP:依赖抽象不依赖细节 → 控制"解耦程度"
五条本质就一句话:
把"变化"圈起来、关进笼子,让它只影响该影响的地方。
23 个 GoF 模式,全是这条原则的具体套路。
|
五、23 个模式分类速览
1
2
3
4
5
6
7
8
9
10
11
12
| 创建型(Creational)—— 怎么造对象
单例、工厂方法、抽象工厂、建造者、原型
结构型(Structural)—— 怎么组合对象
适配器、装饰器、代理、外观、组合、桥接、享元
行为型(Behavioral)—— 对象怎么协作
策略、责任链、观察者、命令、状态、模板方法、
迭代器、中介者、备忘录、访问者
本系列挑后端最常用的讲(不追求 23 个全覆盖):
策略 / 工厂 / 建造者 / 装饰器 / 责任链 / 观察者 / 单例 / 命令 / 状态
|
六、.NET 里模式的"现代形态”
1
2
3
4
5
6
7
8
9
10
11
12
13
| 很多模式在 .NET 里已经"融化"进语言/框架,不用手写类层次了:
策略 → Func<T> / delegate / 字典派发(下一篇详讲)
工厂 → DI 容器(IServiceProvider)天然就是
观察者 → event / 委托 / IObservable<T> 内置
迭代器 → foreach + IEnumerable<T>,语言级支持
单例 → DI 的 Singleton 生命周期,一行注册
建造者 → 链式 API(EF Core ModelBuilder 等)随处可见
启示:学模式时,要分清"模式的意图"和"模式的经典实现"。
意图不变(解耦算法、封装创建、订阅变化…),
实现可以很现代、很简洁。
死记 UML 类图没用,懂了意图,C# 三行就能写出来。
|
七、小结
- 模式 = 有名字的设计经验,首先是沟通词汇,不是必须照抄的答案
- 别滥用:模式应对真实反复的复杂度,不是想象中的(YAGNI)
- SOLID 是底座:SRP/OCP/LSP/ISP/DIP——本质都是"把变化关进笼子”
- DIP 最重要:依赖抽象不依赖细节,是整洁架构和 DI 的根基
- 23 个模式分创建/结构/行为三类;本系列挑后端最常用的 9 个(+SOLID)
- .NET 里模式常已融化进语言:学意图,别死记 UML
下一篇从最常用的策略模式开始——看它怎么把一坨 if/switch 变成干净的算法族。