设计模式实战(一):总览——模式的本质与 SOLID

写在前面

我写了 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 变成干净的算法族。