
先给个结论这篇 C# 设计模式总目录不是让你背 23 个类图的。真正的工作场景里你可能一年都用不上其中一半但如果你能把每个模式的“意图”装进脑子里等到代码开始发臭的时候你会第一时间闻到味道并且知道该往哪个方向重构。这才是设计模式对一个 C# 开发者的真实价值。我用实际工程经验把 GoF 的 23 种模式重新梳理了一遍结合 C# 的语言特性把每个模式最容易踩的坑、最常用的落地姿势、以及哪些模式其实已经被语法糖“吸收”掉的部分都列出来了。这份目录适合三种人准备面试的、正在做设计模式大作业/期末复习的、以及写业务代码写到想重构又不知道从哪下手的。1. 23 种模式不是 23 个考点先建立你的模式地图很多人学设计模式喜欢按“创建型、结构型、行为型”这个分类顺序从头看到尾然后看完前三个就放弃了。原因很简单分类是对的但它只回答了“模式属于哪一类”没有回答“我什么时候该想到它”。我建议换个角度先搞清楚三大家族的本质区别。创建型模式5 种解决的是“对象怎么来”的问题。核心矛盾是构造过程太复杂、太直接、太依赖具体类型导致调用方被耦合死。单例、工厂方法、抽象工厂、建造者、原型本质上都是在“把 new 这件事管起来”。结构型模式7 种解决的是“对象怎么组合”的问题。类之间继承关系太僵、接口不匹配、粒度太细导致对象满天飞这些问题由适配器、桥接、组合、装饰器、外观、享元、代理来处理。行为型模式11 种解决的是“对象之间怎么协作、算法怎么组织”的问题。责任链、命令、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者、解释器处理的全是运行时行为。这三类有个朴素的对应关系创建型管出生结构型管搭骨架行为型管过日子。1.1 先看一张速查表再决定从哪里切入我按“实际工作中最常用”的程度重新排了个序和教科书顺序不太一样优先级模式一句话本质C# 里最常见的落点必学观察者状态变化时通知所有关注者event / delegate必学策略把可替换的算法族封装成对象FuncT、IComparerT、DI 注入必学工厂方法让子类决定实例化哪个类依赖注入容器必学装饰器不修改原类动态叠加职责Stream 派生体系、管道中间件必学责任链请求沿链路传递直到有人处理ASP.NET Core 中间件必学模板方法骨架固定步骤留给子类实现抽象基类 virtual 方法高频单例全局唯一实例配置对象、日志写入器高频适配器让不兼容的接口协作对接第三方库、老系统高频外观给复杂子系统一个简单入口Program.cs、Facade 类高频迭代器统一遍历集合的方式foreach / yield return高频代理控制对真实对象的访问EF Core 延迟加载高频命令把操作封装成对象Undo/Redo、任务队列中频状态用类表示状态行为随状态切换状态机、订单流程中频建造者分步骤构造复杂对象Fluent API、WebApplicationBuilder中频抽象工厂创建一族相关对象跨平台 UI 控件中频桥接抽象与实现各自独立演化跨平台渲染、多数据库驱动中频组合树形结构统一处理叶子与节点WinForms/WPF 控件树中频享元共享细粒度对象减少内存字符串驻留、对象池中频中介者对象之间不直接通信由中介转发复杂窗体控件协作中频备忘录保存对象状态以便恢复序列化快照可选原型通过复制已有对象创建新对象MemberwiseClone可选访问者在不改类的前提下增加新操作ExpressionVisitor可选解释器定义语法并解释执行正则引擎、DSL 解析这张表不是标准答案但它是按我的经验排的。学的时候优先看“必学”和“高频”这 10 个吃透了你的日常开发已经比别人舒服太多。1.2 学设计模式最大的误区上来就背类图类图是模式的结果不是模式的本质。如果你问一个新人“单例模式是什么”他能画出来构造函数私有、静态属性返回实例但你再问他“为什么这里要用单例”他可能就卡住了。真正该记的是“这个模式解决了什么矛盾、在什么场景下会想到它、代价是什么”。带着这个思路看后面的章节你会发现 23 个模式其实是在反复回答同一批问题对象怎么创建、类怎么组合、行为怎么协作。2. 创建型模式先回答“对象怎么来”创建型模式虽然是学设计模式的第一站但它在实际 C# 项目里的存在感已经被 DI 容器冲淡了不少。比如工厂方法如果项目里用了 Autofac 或者微软自带的IServiceProvider很多工厂类的活儿容器直接帮你干了。但这里有个隐藏前提你仍然需要理解工厂背后的意图否则容器用起来也是一头雾水。2.1 单例与原型两个容易被玩坏的模式单例可能是被滥用得最严重的模式。它的核心场景只有两种全局唯一且有状态的对象比如配置中心、日志写入器以及创建成本极高且无状态的对象比如数据库连接工厂。C# 里最稳妥的写法不是双重检查锁而是直接用LazyT把线程安全交给运行时去保证public sealed class ConfigManager { private static readonly LazyConfigManager instance new LazyConfigManager(() new ConfigManager()); public static ConfigManager Instance instance.Value; private ConfigManager() { // 加载配置文件 } }注意两个坑第一单例对象如果持有可变的全局状态等于把全局耦合点埋进了所有引用它地方测试的时候想隔离都没法隔离第二sealed关键字一定要加否则别人继承你的单例类重写方法全局唯一性直接就破了。原型模式在 C# 里存在感最低因为ICloneable接口设计得比较别扭它既不告诉你返回值是深拷贝还是浅拷贝也没有泛型版本。实现的时候要特别小心引用类型成员一个MemberwiseClone默认是浅拷贝类里如果有ListT、数组、自定义对象拷贝出来的两个对象会共享同一份引用改一个等于全改。需要深拷贝的时候我常用的方案是序列化拷贝内存开销大一些但省心public T DeepCloneT(T source) { var json JsonSerializer.Serialize(source); return JsonSerializer.DeserializeT(json)!; }2.2 工厂方法、抽象工厂与建造者三个制造对象的思路工厂方法解决的是一个很具体的矛盾调用方不想依赖“具体的产品类”只想依赖“创建产品的接口”。落到 C# 里最典型的例子是日志组件ILoggerFactory定义创建ILogger的方法具体的ConsoleLoggerProvider、FileLoggerProvider各自实现自己的创建逻辑调用方这辈子只需要跟ILogger打交道。这种模式下新增一种日志提供方不用改任何调用代码只加一个新工厂类就行。抽象工厂比工厂方法再进一步它制作的是“一族”产品。比如一个跨平台报表系统Windows 下要生成 Windows 风格的按钮macOS 下要生成 macOS 风格的按钮抽象工厂就定义CreateButton()、CreateTextBox()等一整套接口每个平台都有自己的实现。C# 里现在做跨平台 UI 更多用 MAUI 这类框架框架内部帮你搞定了这套抽象但你去看 MAUI 源码底层依然是抽象工厂的思路。建造者模式在 C# 里收获了一个非常舒服的落地场景Fluent API。StringBuilder就是一个教科书级例子——直接 new 一个字符串对象然后不断 append性能会非常难看用建造者的思路把“构建过程”和“最终产物”拆开中间状态都留在 Builder 内部。到了 ASP.NET Core 里WebApplicationBuilder也是典型建造者builder.Services.AddXxx()、builder.Build()这套流程就是标准的分步配置 最终构建。2.3 创建型模式的 C# 落地技巧把 new 收拢到一处创建型模式的出发点最终都指向同一个原则别让 new 散落在业务代码里。你可以在业务代码里看到一个new OrderService()然后改成new OrderService(new OrderRepository(new DbContext()))这种连锁改动的痛苦相信写过几年代码的人都懂。工厂方法、抽象工厂、建造者本质上都是把这种“构造细节”收拢到独立的类里让上层只看到一个简单的创建入口。在 C# 里这个入口现在多数时候就是 DI 容器所以我的建议是单例用LazyT实现注册进容器时用单例生命周期别自己写静态属性满天飞。工厂方法如果项目用了 DI优先把FuncT注册进容器作为工厂委托绕开自定义工厂类。建造者大量用于配置对象、HTTP 请求构造、复杂查询条件的封装C# 里写 Fluent API 很顺手注意每一步返回this即可。抽象工厂只在确实存在“一族产品”需要配套创建时使用单产品直接用工厂方法别为了设计模式而设计模式。原型说实话业务代码里我很少主动用它但它和System.Text.Json的深度拷贝配合起来在做快照和回滚功能时很实用。3. 结构型模式怎么组装出灵活的对象骨架创建型管的是“对象出生”结构型管的是“对象之间的关系”。这类模式里装饰器、适配器、代理这三个特别容易混淆因为它们看起来都像是“包了一层”。区分的方法其实很直接适配器是接口不匹配时用来翻译代理是不想直接碰真实对象时用来控制访问装饰器是想给对象动态加能力而不改类。3.1 装饰器、适配器与代理三个“包装类”的边界在哪C# 里装饰器最经典的案例就是Stream体系。FileStream负责读写文件字节流BufferedStream给它加缓冲GZipStream给它加压缩CryptoStream给它加加密而且可以任意组合using var file File.OpenRead(data.bin); using var gzip new GZipStream(file, CompressionMode.Decompress); using var reader new StreamReader(gzip);注意这里有个 C# 细节Stream 的装饰器模式用得非常“原生”每个装饰器接收一个已有 Stream再对外暴露同样的 Stream 接口所以层层包裹之后调用方拿到的仍然是个 Stream可以继续套下一层。写业务代码时如果某个类的职责需要被动态叠加装饰器是最好的选择C# 里也常常用扩展方法来实现类似效果。适配器的典型场景是“第三方库的接口和我们系统里的接口对不上”。比如系统里定义了一个IReportExporter而第三方报表库只有PdfExporter.Export(string content)你不能硬改第三方库那就做个适配器内部包装第三方库外部实现自己的接口。这个模式在对接老系统、旧设备 SDK、外部 API 时特别常见。代理模式在 C# 里最有名的就是 EF Core 的延迟加载你的实体类属性是virtual的EF Core 运行时动态生成代理类当你访问导航属性时代理类才真正去数据库查询。日常开发中懒加载、访问控制、调用日志这三个场景最适合用代理。注意区分动态代理和静态代理EF Core 用的是 Castle 动态代理运行时生成你自己写日志代理时手写一个静态代理类往往更直观、也更好调试。3.2 组合、桥接与外观把“复杂度”藏起来组合模式处理的是树形结构。C# 里 WinForms/WPF 的控件树就是最典型的组合模式控件既可以是一个叶子节点Button、TextBox也可以是一个容器节点Panel、Grid它们都继承自同一个Control基类所以处理的时候统一按Control来操作叶子节点和容器节点的差异被隐藏了。写递归菜单、组织架构、文件夹系统时都应该想到它。关键点是叶子节点和容器节点必须实现同一个接口否则调用方就要写一堆if判断当前节点是不是叶子节点了。桥接模式解决的是“一个事物有两个维度都在变化”的问题。比如消息发送器维度一是发送渠道短信、邮件、微信维度二是消息内容类型普通文本、富文本、带附件。如果只用继承类数量会爆炸成 3×39 个用桥接模式把其中一个维度抽象成独立的接口解耦后只需要 3 个渠道类 3 个消息类。在 C# 里桥接最典型的例子是数据库驱动DbConnection和DbCommand各是一个抽象底层由SqlConnection/SqlCommand、NpgsqlConnection/NpgsqlCommand组合成一套完整的实现。外观模式是最容易理解的模式之一给复杂子系统提供一个简单的门面。ASP.NET Core 的Program.cs就是整个框架的外观你不需要知道 Kestrel 怎么启动、中间件管道怎么组装、依赖注入容器怎么构建只需要调用builder.Build()和app.Run()。写业务代码时如果某个模块有五个类协同工作调用方每次都得 new 一遍、调五次方法那就该提供一个门面类把这五步封装成一个方法。3.3 享元模式C# 里的隐形玩家享元模式的核心是“共享细粒度对象减少重复创建”。C# 里最生动的例子是字符串驻留string a abc; string b abc;在大多数情况下a 和 b 指向的是托管堆上的同一块内存这是 CLR 层面的享元。游戏开发里的粒子系统则是主动使用享元的典型成千上万个粒子如果每个都有自己的贴图、材质、状态对象内存立刻爆炸做法是把贴图和材质这些不变的属性抽成享元共享每个粒子只保留位置、速度这些变化的属性。日常业务开发中享元的常见落点是对象池。比如数据库连接池、线程池、ArrayPoolT本质上都在做同一件事昂贵且可复用的对象不销毁而是回收复用。如果你的系统里大量重复创建不可变对象并且这些对象创建成本很高就该想想享元模式了。但要注意享元模式的代价是共享对象不能有可变状态否则一个地方改了所有引用方全部跟着变排查起来非常痛苦。4. 行为型模式把算法与协作规则沉淀下来行为型模式占了 23 种里的 11 种是数量最多、也最能体现设计功力的一个家族。很多人学到这一块就迷路了因为它不像创建型和结构型那么“结构化”更偏“运行时如何协作”。好消息是C# 的委托和事件已经把好几个行为型模式“语法化”了学的时候会有一种“原来我早就会了”的感觉。4.1 委托时代观察者、命令、策略在 C# 里被简化成了什么观察者模式在 C# 里约等于event。发布者定义事件订阅者 一个方法事件触发时订阅者自动收到通知。这是 C# 对观察者模式最彻底的吸收日常写按钮点击事件、数据变更通知、进度上报全都在用它。但我必须提醒一个隐蔽的坑事件监听会产生强引用导致对象无法被垃圾回收。如果你的发布者生命周期很长比如全局事件总线订阅者频繁创建销毁那个的实例方法会一直绑住订阅者内存涨上去之后查起来非常费劲。解决办法是长期订阅的类在 Dispose 时记得-或者使用弱事件模式。命令模式的核心是把“要执行的操作”封装成对象从而支持排队、撤销、重做、日志。C# 的Action/FuncT让很多命令场景直接改用委托就够了不需要再建一堆命令类。但委托解决的只是“把方法当参数传”如果你的命令需要支持 Undo就必须有Execute和Undo两个方法这时候委托就不够了需要正经的命令类。做编辑器、工作流引擎、批量操作时命令模式仍然是首选。策略模式在 C# 里被简化为一个委托或者一个接口。比如排序时传入IComparerTLINQ 里传入FuncT, bool做过滤这就是策略模式的日常形态。如果策略只有一行实现用委托就够了如果策略包含多个步骤且有状态再定义策略接口。4.2 管道思维责任链、模板方法与状态机责任链模式在 C# 里最出名的案例就是 ASP.NET Core 中间件管道请求进来依次经过每一个中间件每个中间件可以选择处理完就返回也可以调用next()传给下一个。这个模式解决的是“请求处理者的解耦”每个处理器只关心自己能不能处理处理不了就往后传。日志级别过滤、异常处理链、审核审批流结合工单系统场景都是它在业务里的应用。写责任链的时候需要注意和装饰器一样都是层层包裹但关注点不同。装饰器关注的是“在原有功能上叠加能力”责任链关注的是“谁能处理就谁来处理处理不了就往后传”。模板方法模式解决的是“算法骨架固定但某些步骤可以变化”的问题。C# 里最简单的实现就是抽象基类的方法内部调用虚方法子类只重写需要变化的步骤public abstract class DataImporter { public void Import(string file) { ValidateFile(file); var data Parse(file); Transform(data); Save(data); } protected virtual void ValidateFile(string file) { } protected abstract ListT Parse(string file); protected virtual void Transform(ListT data) { } protected abstract void Save(ListT data); }模板方法和策略的区别很微妙但很重要模板方法是在基类里定好算法流程把变化的步骤留给子类去改是“继承”思路策略是算法整体都可以替换是“组合”思路。判断标准很简单——只有个别步骤会变用模板方法整个算法都可能换用策略。状态模式是状态机的面向对象实现。核心思想把每个状态封装成独立的类对象内部持有一个当前状态对象状态迁移时就切换持有对象行为随状态自然改变。订单状态流转待支付、已支付、已发货、已完成是它的常见场景。实际业务里我会做个简化不建一堆状态类而是用字典映射状态和处理器委托实现同样清晰。4.3 迭代器、中介者、备忘录、访问者、解释器不该忽视的实用派迭代器模式在 C# 里已经和语法融为一体了。foreach能遍历任何实现了IEnumerableT的集合yield return能让你轻松编写自定义迭代器这在其他语言里是核心框架能力在 C# 里直接内置了。理解迭代器模式的价值在于当你自己写一个自定义集合类时知道实现IEnumerableT而不是提供一个GetList()方法让外界拿内部容器。中介者模式解决的是“多个对象互相引用导致网状耦合”的问题。WinForms 里两个用户控件要通信很多人图省事直接在控件 A 里引用控件 B结果界面一复杂牵一发而动全身。正确做法是有一个中介者比如窗体本身来转发消息控件之间不直接引用。C# 里游戏开发中UI 管理器、网络管理器、战斗系统之间的松耦合通信用事件总线做中介者也是常见套路。备忘录模式是“保存对象状态稍后恢复”。C# 里实现成本很低序列化一把梭就行。注意设置快照时要考虑完整性不用每个字段都存关键是能恢复到需要的程度。编辑器的撤销功能、游戏的存档读档都是备忘录模式的实践。访问者模式在日常业务代码中用的最少但在编译器、表达式树这些场景里非常关键。C# 里ExpressionVisitor就是访问者模式的直接体现LINQ Provider 解析表达式树、AOP 框架遍历代码结构时频繁使用。它的妙处是在不修改现有类的前提下为这些类增加新的操作——把“在元素类里加方法”改成“在访问者里加方法”。代价是新增元素类时所有访问者都得跟着改所以不适合会频繁增加元素类型场景。解释器模式定义了“没有语法就没有解释器”。C# 里最接地气的例子是正则表达式引擎你把正则字符串交给引擎引擎解释它并执行匹配。业务代码中如果要实现一个简单的规则引擎比如优惠规则、动态表单校验规则解释器模式是个不错的参考但要注意语法一旦复杂解释器的维护成本会指数级上升能用现成表达式库比如System.Linq.Dynamic.Core就别自己写。5. C# 语法与设计模式的互相成就这一节很多人没想过C# 语言本身其实在不断吸收设计模式把它们变成语法糖。所以你在 C# 里“用设计模式”的姿势应该和 C/Java 很不一样。认清楚这一点写出来的代码才不会被GoF原版类图带跑偏。5.1 语言特性就是模式的最终落点委托/事件观察者模式、命令模式的很多场景被委托直接替代。你不必为“鼠标点击”定义一个观察者接口和一组订阅者直接用event EventHandler就完了。yield return迭代器模式的语法化。你再也不需要手写一个IEnumeratorT类写出一个带yield return的方法编译器自动生成迭代器。扩展方法装饰器模式的轻量实现。不想改原类又想给它加能力写个静态类加扩展方法就行比装饰器类更轻。泛型让策略模式、模板方法模式更安全。IComparerT就是策略接口FuncT, T是策略委托配合泛型可以少写很多类型转换。using/IDisposable模板方法模式在资源管理领域的实现。using语句保证Dispose一定执行释放资源的骨架由语言保证你只需要实现Dispose这一个步骤。依赖注入容器工厂模式的大成。容器接管了“创建谁、何时创建、生命周期多长”工厂类退居二线变成容器的注册项。5.2 当模式遇上语法糖哪些 GoF 类图该“翻译”成 C# 写法学习 23 种模式时千万别直接照着 GoF 的 C/Java 类图写 C# 代码那样写出来的代码又臭又长完全不符合现代 C# 风格。我总结了一个翻译对照GoF 原版意图C# 推荐写法观察者Subject/IObserver 接口event / EventHandlerT或 IObservableT命令Command 接口 ConcreteCommandAction / FuncT复杂场景才用命令类策略Strategy 接口 ConcreteStrategyFuncT 委托或 DI 注册不同策略实现迭代器Iterator 接口 ConcreteIteratorIEnumerableT / yield return模板方法abstract 子类继承同左注意搭配 sealed 防止骨架被破坏工厂方法Creator/Product 层次结构直接用 DI 容器注册对应的服务泛型装饰器Decorator 类继承 组合扩展方法 中间件管道这不是说 GoF 过时了恰恰相反正因为它提炼的模式足够本质才能被语言设计者吸收成语法。学模式的时候如果只背类图你会觉得 C# 把模式变得没有“仪式感”了但你理解了意图之后会发现 C# 的语法糖让你更轻松地表达这些意图而不是被类图绑架。6. 学习路径与实战建议从目录到落地拿到这份目录之后下一步怎么办我的建议是不要从头到尾逐章看而是按下面的路径走。6.1 面向面试和作业这样速成最快如果你是为了面试或 C# 面试题准备优先掌握这 10 个单例、工厂方法、抽象工厂、建造者、适配器、装饰器、代理、观察者、策略、模板方法。这十个占了面试题和设计模式期末考试的绝大部分。每个模式用“三步法”学先看一遍核心意图和场景再手写一个 C# 最小实现不要抄凭记忆写最后找一个实际业务场景套进去。比如单例模式就想想日志类策略模式就想想排序比较器。能做到给场景就能说出模式给模式就能画出基本结构面试基本够用了。如果是做设计模式大作业建议用“一个整体项目串起多个模式”的思路比如搭建一个简易的订单管理系统工厂方法创建订单、单例管理配置、观察者通知库存、策略计算运费、模板方法定义处理流程比堆 23 个独立例子有说服力得多。6.2 什么时候真的不要用设计模式这个可能比“什么时候该用”更重要。我踩过的坑一个分页查询写得太复杂为了“可扩展”硬套抽象工厂结果新增一个查询条件要改三个类文件团队其他人看代码直接崩溃。设计模式的代价是增加类的数量和间接层如果你的业务场景可能未来两年都不会变化简单直接的代码比“优雅可扩展”的代码好得多。我的判断标准很简单当代码出现实际的坏味道时才用模式去重构而不是先套一层模式预判未来可能会用到。“未来可能扩展”是最大的伪需求。6.3 怎么验证你是不是真的会了有一个很有效的自测方法每个模式找一个你在真实业务里写过的场景用这个模式重写一遍然后对比重构前后的差异。如果你的代码量没有减少、可读性没有提升、测试难度没有下降那说明这个模式在你的场景里并不必要。学设计模式不是为了在代码里炫耀类图而是为了在代码演进到某个节点时能顺着“坏味道”找到最合适的解法。目录里的 23 种模式是 23 个工具箱不是 23 个装饰品。就我个人而言学 C# 设计模式这些年最大的变化不是代码变得“高级”了而是面对烂代码的时候不再只会骂街——看到类里边职责太多就想到用外观模式拆门面看到一个方法里 if else 堆了十几个分支就想到策略模式替换看到事件绑定忘了解绑导致内存泄漏就想到观察者模式的强引用陷阱。设计模式的价值不是让你写出别人看不懂的“高级代码”而是让你在代码变臭的时候能精确地判断出哪里在臭、该用什么手段处理。这份目录我根据自身实战经验重新梳理了一遍建议你收藏下来等到代码发臭的时候再回来看对应的模式那时你才能真正体会到它的作用。