文章目录
- 1.创建型(解决"怎么把对象造出来"的问题)
-
- [1. 单例模式 (Singleton)](#1. 单例模式 (Singleton))
-
- [1.1. 饿汉模式 (Eager Initialization)](#1.1. 饿汉模式 (Eager Initialization))
- [1.2. 懒汉模式 (Lazy Initialization)](#1.2. 懒汉模式 (Lazy Initialization))
- [1.3. 两种模式对比](#1.3. 两种模式对比)
- [1.4. 推荐写法:`Lazy<T>`](#1.4. 推荐写法:
Lazy<T>)
- [2. 工厂模式 (Factory)](#2. 工厂模式 (Factory))
-
- [2.1. 简单工厂 (Simple Factory)](#2.1. 简单工厂 (Simple Factory))
- [2.2. 工厂方法 (Factory Method)](#2.2. 工厂方法 (Factory Method))
- [2.3. 抽象工厂 (Abstract Factory)](#2.3. 抽象工厂 (Abstract Factory))
- [2.4. 概念解释](#2.4. 概念解释)
- [2.5. 总结对比](#2.5. 总结对比)
- 2.结构型(解决"如何组合对象、处理接口兼容"的问题)
-
- [3. 依赖注入 (Dependency Injection - DI)](#3. 依赖注入 (Dependency Injection - DI))
-
- [3.1. 场景对比:发送消息通知](#3.1. 场景对比:发送消息通知)
-
- [不使用 DI(硬编码耦合)](#不使用 DI(硬编码耦合))
- [使用 DI(解耦)](#使用 DI(解耦))
- [3.2. 依赖注入的实现流程](#3.2. 依赖注入的实现流程)
- [3.3. 在 .NET 环境中配置 DI 容器](#3.3. 在 .NET 环境中配置 DI 容器)
- [4. 装饰器模式 (Decorator)](#4. 装饰器模式 (Decorator))
-
- [4.1. 场景:咖啡订单系统](#4.1. 场景:咖啡订单系统)
- [5. 适配器模式 (Adapter)](#5. 适配器模式 (Adapter))
-
- [5.1. 场景:第三方 SDK 接入](#5.1. 场景:第三方 SDK 接入)
- [5.2. 类图](#5.2. 类图)
- [5.3. 关键概念](#5.3. 关键概念)
- [5.4. 适配器 vs 装饰器 (容易搞混)](#5.4. 适配器 vs 装饰器 (容易搞混))
- [5.5. 实际应用](#5.5. 实际应用)
- [5.6. 代理模式 (Proxy)](#5.6. 代理模式 (Proxy))
- [3. 行为型(解决"对象之间如何沟通、执行算法"的问题)](#3. 行为型(解决“对象之间如何沟通、执行算法”的问题))
-
- [6. 观察者模式 (Observer)](#6. 观察者模式 (Observer))
- [7. 策略模式 (Strategy)](#7. 策略模式 (Strategy))
-
- [7.1. 场景:商场促销折扣](#7.1. 场景:商场促销折扣)
- [7.2. 类图](#7.2. 类图)
- [7.3. 关键概念](#7.3. 关键概念)
- [7.4. 策略模式 vs 工厂模式](#7.4. 策略模式 vs 工厂模式)
- [7.5. 轻量级策略](#7.5. 轻量级策略)
- [4. 模式选择流程图](#4. 模式选择流程图)
- [5. 示例](#5. 示例)
-
- [DI + 工厂模式](#DI + 工厂模式)
-
- [1. 定义接口(抽象)](#1. 定义接口(抽象))
- [2. 具体实现](#2. 具体实现)
- [3. 智能工厂(配合 DI 容器)](#3. 智能工厂(配合 DI 容器))
- [4. 注册与使用](#4. 注册与使用)
- [单例模式中 `Lazy<T>` 的线程安全实现 **&** 观察者模式 在 .NET 中的事件模型](#单例模式中
Lazy<T>的线程安全实现 & 观察者模式 在 .NET 中的事件模型) -
- [1. 深度单例:Lazy<T>](#1. 深度单例:Lazy<T>)
- [2. 观察者模式:事件流](#2. 观察者模式:事件流)
- [6. 避坑](#6. 避坑)
-
- [6.1. 建议](#6.1. 建议)
本文介绍C#中常用的设计模式
1.创建型(解决"怎么把对象造出来"的问题)
1. 单例模式 (Singleton)
- 本质: 核心资源的"独苗"。
- 详细介绍: 确保一个类只有一个实例,并提供一个全局访问点。
- 专家视点: 在 C# 中,一定要注意线程安全 。目前最推荐的做法是利用 .NET 提供的
Lazy<T>类型,它原生支持线程安全且实现了懒加载。 - 应用:
Logger日志组件、Configuration配置读取器、Database Connection Pool连接池。
它的核心本质不是"节省内存",而是**"状态的一致性"**。如果一个配置类在 A 处修改了,B 处读到的还是旧值,系统就乱套了。
1.1. 饿汉模式 (Eager Initialization)
核心原理: 类加载时就立即创建实例。就像一个极其饥饿的人,饭还没端上来就早早守在桌边。
- 优点: 线程安全(由 .NET 运行时保证),没有锁的开销,执行效率高。
- 缺点: 即使程序从头到尾没用到这个实例,它也会占用内存资源。
csharp
public sealed class EagerSingleton
{
// 静态字段在类加载时初始化
private static readonly EagerSingleton _instance = new EagerSingleton();
// 私有构造函数,防止外部实例化
private EagerSingleton() { }
public static EagerSingleton Instance => _instance;
}
1.2. 懒汉模式 (Lazy Initialization)
核心原理: 第一次调用 Instance 属性时才创建实例。就像一个懒惰的人,不到饿晕那一刻绝不去厨房。
- 优点: 节省资源,实现延迟加载 (Lazy Loading)。
- 缺点: 需要处理多线程安全 问题。如果两个线程同时判断
_instance == null,可能会创建出两个实例。
双重检查锁定
这是懒汉模式在多线程环境下的标准写法:
csharp
public sealed class LazySingleton
{
private static LazySingleton _instance = null;
private static readonly object _lock = new object();
private LazySingleton() { }
public static LazySingleton Instance
{
get
{
// 第一重检查:避免不必要的加锁
if (_instance == null)
{
lock (_lock)
{
// 第二重检查:确保只创建一个实例
if (_instance == null)
{
_instance = new LazySingleton();
}
}
}
return _instance;
}
}
}
1.3. 两种模式对比
| 特性 | 饿汉模式 (Eager) | 懒汉模式 (Lazy) |
|---|---|---|
| 创建时机 | 类加载时 (Static Constructor) | 首次使用时 |
| 线程安全 | 天生安全 | 需手动加锁 (Double-Check) |
| 内存利用 | 可能浪费内存 | 只有需要时才占内存 |
| 性能 | 获取实例速度最快 | 首次获取有开销(锁/创建) |
1.4. 推荐写法:Lazy<T>
在 .NET 开发中,我们不再手动写双重检查锁定,而是直接使用内置的 Lazy<T> 类,它既实现了懒加载,又是线程安全的。
csharp
public sealed class BestSingleton
{
private static readonly Lazy<BestSingleton> _lazy =
new Lazy<BestSingleton>(() => new BestSingleton());
private BestSingleton() { }
public static BestSingleton Instance => _lazy.Value;
}
2. 工厂模式 (Factory)
工厂模式(Factory Pattern)的核心思想是:
将对象的创建与使用分离。简单来说,你不需要自己去 new 一个对象,而是告诉"工厂"你需要什么,由工厂负责把成品给你。
在 C# 开发中,我们通常将其分为三个层级:简单工厂、工厂方法和抽象工厂。
- 本质: 对象的"外卖员"。你只管下单,不用管怎么厨师怎么炒菜。
- 详细介绍: 定义一个创建对象的接口,让子类决定实例化哪一个具体类。
- 专家视点: 当你的类构造函数里参数太多,或者创建逻辑很复杂时,用工厂包裹起来能让调用方的代码干净很多。
- 应用: 跨平台 UI 控件库(根据系统创建 Windows 按钮或 Mac 按钮)、不同类型的订单创建。
核心诉求是**"解耦创建过程"**。当 new 一个对象变得复杂(比如需要读配置、初始化各种参数)时,就不该让业务层去承担这些逻辑。
- 实践逻辑:

- 开闭原则 (OCP - Open/Closed Principle):对扩展开放(可以加新订单类型),对修改关闭(不需要动原有的工厂判断逻辑)。
2.1. 简单工厂 (Simple Factory)
本质: 一个中心化的"分发器"。它不是一种正式的设计模式,而是一种编程习惯。
场景: 假设你正在开发一个日志系统,需要根据配置创建"数据库日志"或"文件日志"。
csharp
public enum LogType { File, Database }
public class LogFactory
{
public static ILog CreateLog(LogType type)
{
return type switch
{
LogType.File => new FileLog(),
LogType.Database => new DatabaseLog(),
_ => throw new ArgumentException("无效类型")
};
}
}
- 优点: 客户端代码不需要知道具体类的实现。
- 缺点: 违背了 开闭原则(Open-Closed Principle) 。每增加一种日志类型,你都必须修改
CreateLog方法的switch分支。
2.2. 工厂方法 (Factory Method)
本质: 定义一个创建对象的接口,但由子类决定实例化哪一个类。
第一性原理: 解决简单工厂"违反开闭原则"的问题。不再由一个大工厂管所有事,而是为每个产品配备一个专门的小工厂。
csharp
// 工厂接口
public interface ILogFactory { ILog Create(); }
// 文件日志工厂
public class FileLogFactory : ILogFactory
{
public ILog Create() => new FileLog();
}
// 数据库日志工厂
public class DatabaseLogFactory : ILogFactory
{
public ILog Create() => new DatabaseLog();
}
- 优点: 增加新产品时,只需新增一个工厂类,不需要改动现有代码。
- 流程图:

2.3. 抽象工厂 (Abstract Factory)
本质: 提供一个接口,用于创建一系列相关或相互依赖的对象(产品族),而无需指定它们具体的类。
场景: 你的软件需要支持多种皮肤(皮肤 A 和皮肤 B)。皮肤 A 有"圆形按钮"和"白色背景";皮肤 B 有"方形按钮"和"黑色背景"。按钮和背景必须配套使用。
csharp
public interface IUiFactory
{
IButton CreateButton();
IBackground CreateBackground();
}
public class SkinAFactory : IUiFactory
{
public IButton CreateButton() => new RoundButton();
public IBackground CreateBackground() => new WhiteBackground();
}
- 核心逻辑: 它不是生产"一个"产品,而是生产"一套"产品。
2.4. 概念解释
- 开闭原则 (Open-Closed Principle): 软件实体(类、模块等)应该对扩展开放,对修改关闭。工厂方法通过增加新类而不是修改旧代码完美体现了这一点。
- 产品族 (Product Family): 指由同一个工厂生产的,位于不同等级结构中的一组产品(如:华为手机、华为路由器)。
- 解耦 (Decoupling): 调用者(Client)只依赖于接口(Interface),而不依赖于具体的实现类,这使得系统更易于维护和测试。
2.5. 总结对比
| 模式 | 解决的问题 | 复杂度 |
|---|---|---|
| 简单工厂 | 封装创建逻辑,不让 Client 自己 new | 低 |
| 工厂方法 | 解决简单工厂难以扩展(不符合开闭原则)的问题 | 中 |
| 抽象工厂 | 解决多个关联产品的配套创建问题(产品族) | 高 |
2.结构型(解决"如何组合对象、处理接口兼容"的问题)
3. 依赖注入 (Dependency Injection - DI)
这是现代 .NET 开发的灵魂。你不再需要自己 new 对象,而是向框架"声明"你需要什么,框架会在运行时帮你送货上门。
- 它解决了对象的生命周期管理。如果没有 DI,你很难管理一个对象应该是全局唯一的(Singleton)、请求范围内的(Scoped)、还是每次都新建的(Transient)。
- 本质: 对象的"包分配"。我不找对象,让领导(容器)给我发一个。
- 详细介绍: 将对象所依赖的外部资源通过构造函数或属性注入进来,而不是在类内部
new。 - 专家视点: 这在 .NET Core 及后续版本中是灵魂。它彻底解决了类与类之间的硬编码依赖,让单元测试(Mock)变得可能。
- 应用: ASP.NET Core 中
IServiceCollection的各种AddScoped、AddTransient。- 解耦 (Decoupling):降低模块间的关联性。改了 A 层的代码,B 层不需要重新编译或大规模变动。
- 控制反转 (IoC - Inversion of Control):把创建对象的控制权从开发者手里交给框架。
在软件工程中,依赖注入 (Dependency Injection, DI) 的第一性原理是:把对象需要的外部资源(依赖)由"自己创建"改为"由外部送进来"。
如果不使用 DI,类与类之间是"强耦合"的;使用了 DI,类只依赖于"接口",不关心具体实现。
3.1. 场景对比:发送消息通知
假设我们要写一个 OrderService(订单服务),在订单完成后需要发送邮件。
不使用 DI(硬编码耦合)
OrderService 内部直接 new 了一个 EmailService。如果哪天你想改成发送短信,你必须修改 OrderService 的代码。
csharp
public class OrderService
{
private readonly EmailService _emailService = new EmailService();
public void CompleteOrder()
{
// 业务逻辑...
_emailService.Send("订单已完成");
}
}
使用 DI(解耦)
我们定义一个接口 IMessageService。OrderService 只知道它需要一个能发消息的东西,至于具体是邮件还是短信,它不关心。
csharp
public interface IMessageService
{
void Send(string message);
}
public class OrderService
{
private readonly IMessageService _messageService;
// 依赖通过构造函数注入 (Constructor Injection)
public OrderService(IMessageService messageService)
{
_messageService = messageService;
}
public void CompleteOrder()
{
_messageService.Send("订单已完成");
}
}
3.2. 依赖注入的实现流程

3.3. 在 .NET 环境中配置 DI 容器
在C#(如 .NET 6/7/8)中,我们通常在 Program.cs 中配置 DI 容器(Container)。
csharp
var builder = Host.CreateApplicationBuilder(args);
// 1. 注册 (Registration): 告诉容器,看到 IMessageService 就给它一个 EmailService 实例
builder.Services.AddTransient<IMessageService, EmailService>();
builder.Services.AddTransient<OrderService>();
using IHost host = builder.Build();
// 2. 获取 (Resolution): 容器会自动分析 OrderService 的构造函数,发现它需要 IMessageService
// 于是自动创建一个 EmailService 塞进去
var orderService = host.Services.GetRequiredService<OrderService>();
orderService.CompleteOrder();
4. 装饰器模式 (Decorator)
在不改变原对象结构的情况下,动态地给对象增加额外的功能。
- 本质: 对象的"俄罗斯套娃"。不改原有逻辑,往外面包一层功能。
- 详细介绍: 动态地给一个对象添加额外的职责。
- 专家视点: 当你想给一个老方法加功能(比如加缓存、加日志、加权限),又不敢改老代码时,这是最安全的选择。
- 应用:
FileStream嵌套BufferedStream(加缓冲区)、给 Service 方法外层套一层透明的Redis缓存。
4.1. 场景:咖啡订单系统
假设你有一个基础的 Coffee 类。如果你想加糖、加奶、加摩卡,传统的做法是不断继承(如 CoffeeWithMilk),这会导致"类爆炸"。装饰器模式则通过组合来解决。
csharp
// 1. 共同接口 (Component)
public interface ICoffee
{
string GetDescription();
double GetCost();
}
// 2. 被装饰的具体对象 (Concrete Component)
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "普通咖啡";
public double GetCost() => 10.0;
}
// 3. 装饰器基类 (Decorator) - 必须持有一个被装饰者的引用
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
protected CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
public virtual string GetDescription() => _coffee.GetDescription();
public virtual double GetCost() => _coffee.GetCost();
}
// 4. 具体装饰器 (Concrete Decorator) - 加奶
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => base.GetDescription() + " + 牛奶";
public override double GetCost() => base.GetCost() + 2.0;
}

- 组合优于继承 (Composition over Inheritance): 装饰器通过在内部持有一个接口引用来实现功能的扩展,而不是通过修改类的层级结构。这让功能扩展变得非常灵活。
- 透明性 (Transparency): 装饰器和被装饰对象实现同一个接口。对于客户端(Client)来说,它分不清自己拿到的是一个原始咖啡还是一个加了五层料的装饰咖啡。
- 套娃逻辑 (Recursive Structure): 你可以这样调用:
new MilkDecorator(new SugarDecorator(new SimpleCoffee()))。每一层都在执行完自己的逻辑后,去调用下一层的逻辑。
5. 适配器模式 (Adapter)
- 本质: 通俗点说,它就像你出国旅游时带的"电源转换插头"。
- 详细介绍: 把一个类的接口转换成客户端希望的另一个接口。
- 专家视点: 它是解决老旧系统集成、第三方 SDK 接口不统一的良药。
- 应用: 你的系统用
IPrinter接口,但新买的打印机驱动叫PrintMachine(),写个适配器把两者连起来。
适配器模式 (Adapter Pattern) 的第一性原理是:转换 (Conversion)。它的核心作用是将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
5.1. 场景:第三方 SDK 接入
假设你的系统中有一个标准的日志接口 ILogger,但你新买了一个第三方的强力日志组件 SuperLogger,它的方法名和你的接口完全对不上。
接口不兼容
- 你的接口:
Log(string message) - 第三方:
WriteMessageToFile(string text, int level)
使用适配器 (Adapter)
你不需要修改第三方的代码(你也改不了),而是写一个"中间层"来翻译指令。
csharp
// 1. 你的标准接口 (Target)
public interface ILogger
{
void Log(string message);
}
// 2. 第三方类 (Adaptee) - 接口不兼容
public class SuperLogger
{
public void WriteMessageToFile(string text, int level)
{
Console.WriteLine($"[Level {level}] {text}");
}
}
// 3. 适配器 (Adapter) - 核心逻辑在这里
public class LoggerAdapter : ILogger
{
private readonly SuperLogger _superLogger;
public LoggerAdapter(SuperLogger superLogger)
{
_superLogger = superLogger;
}
public void Log(string message)
{
// 把你的 Log 调用 翻译/适配 给第三方的方法
_superLogger.WriteMessageToFile(message, 1);
}
}
5.2. 类图

5.3. 关键概念
- 目标接口 (Target): 当前系统期望使用的接口。
- 被适配者 (Adaptee): 现有的、接口不匹配的类或组件。
- 适配器 (Adapter): 连接两者的桥梁。它实现
Target接口,并在内部调用Adaptee的方法。 - 对象适配器 vs 类适配器: * 对象适配器(推荐): 使用组合(Composition),如上面的例子,通过持有被适配者的引用来工作。
- 类适配器: 使用多重继承(C# 不支持类多继承,但可以通过实现接口+继承类来模拟)。
5.4. 适配器 vs 装饰器 (容易搞混)
虽然它们长得像(都包装了一个对象),但意图完全不同:
| 模式 | 意图 | 特点 |
|---|---|---|
| 适配器 (Adapter) | 转换接口 | 改变接口,让原本不通的能通。 |
| 装饰器 (Decorator) | 增强功能 | 保持接口不变,只是在外面包一层新功能。 |
5.5. 实际应用
在 .NET 开发中,如果你要把一个旧的 DataTable 转换成现有的 List<T> 业务模型,或者在使用三层架构时将 DataModel 映射到 ViewModel,虽然可以用 AutoMapper 等工具,但其底层逻辑都是适配器思想。
当你发现两个现成的组件很好用,但它们的"接口螺丝"对不上时,直接写个 Adapter 类通常是最快且破坏性最小的方案。
5.6. 代理模式 (Proxy)
- 本质: 对象的"经纪人"。你要找大明星,得先跟经纪人谈。
- 详细介绍: 为其他对象提供一种代理以控制对这个对象的访问。
- 专家视点: 重点在于控制。比如在真正查数据库前,代理先检查你有没有权限,或者检查缓存里有没有现成的。
- 应用: EF Core 的延迟加载、远程过程调用(RPC)中的客户端桩(Stub)。
3. 行为型(解决"对象之间如何沟通、执行算法"的问题)
6. 观察者模式 (Observer)
观察者模式 (Observer Pattern) 的第一性原理是:发布-订阅 (Publish-Subscribe)。
它定义了一种一对多的依赖关系,让多个"观察者"对象同时监听某一个"主题"对象。当这个主题对象的状态发生变化时,它会通知所有观察者,使它们能够自动更新。
在 C# 中,我们几乎不需要手动实现原始的观察者模式,因为语言内置的 委托 (Delegate) 和 事件 (Event) 就是观察者模式的优雅实现。
- 本质: 对象的"订阅号"。一发消息,所有粉丝都收到。
- 详细介绍: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
- 专家视点: 在 C# 中,
event关键字和Action委托本质上就是观察者模式的语法糖。 - 应用: 股票行情实时刷新、用户注册成功后同时发送邮件、短信、优惠券。
6.1. 场景:气象站报警
假设你有一个气象站(Subject),当温度变化时,需要通知"手机 App"和"公告板"(Observers)。

原始模式实现 (手动维护列表)
csharp
// 1. 观察者接口 (Observer)
public interface IObserver {
void Update(float temp);
}
// 2. 主题接口 (Subject)
public interface ISubject {
void Register(IObserver observer);
void Unregister(IObserver observer);
void Notify();
}
// 3. 具体主题
public class WeatherStation : ISubject {
private List<IObserver> _observers = new List<IObserver>();
private float _temp;
public void SetTemperature(float temp) {
_temp = temp;
Notify(); // 状态变了,通知大家
}
public void Register(IObserver o) => _observers.Add(o);
public void Unregister(IObserver o) => _observers.Remove(o);
public void Notify() {
foreach (var o in _observers) o.Update(_temp);
}
}
6.2. 类图

6.3. 使用 event
在实际的 C# 开发(如 WPF 或 WinForms)中,我们直接用事件。这不仅代码量更少,而且更安全。
csharp
public class WeatherStation
{
// 定义事件(本质上就是自动管理的观察者列表)
public event Action<float> OnTemperatureChanged;
public void SetTemperature(float temp)
{
// 如果有人订阅了,就触发
OnTemperatureChanged?.Invoke(temp);
}
}
// 使用时:
var station = new WeatherStation();
// 订阅 (注册观察者)
station.OnTemperatureChanged += (temp) => Console.WriteLine($"手机收到更新:{temp}");
station.OnTemperatureChanged += (temp) => Console.WriteLine($"面板收到更新:{temp}");
6.4. 模式对比:观察者 vs 中介者
| 模式 | 关注点 | 通信方式 |
|---|---|---|
| 观察者 | 状态改变的传播 | 一对多,直接订阅 |
| 中介者 | 多个对象间的复杂交互 | 多对多,通过"中间人"转发,对象间互不认识 |
6.5. 响应式编程 (Rx.NET)
如果你在处理非常复杂的、流式的异步数据,建议了解一下 Observable (IObservable)。它是观察者模式的进化版,支持 LINQ 操作符(如 Filter, Map),是目前处理复杂 UI 事件流的顶级方案。
7. 策略模式 (Strategy)
策略模式 (Strategy Pattern) 的第一性原理是:算法的封装与替换。
它定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换。这让算法的变化独立于使用算法的客户。
简单来说,就是把"做什么"和"怎么做"分开。你只需要告诉程序"我要计算运费",至于怎么计算(顺丰、邮政还是京东),由具体的"策略"决定。
- 本质: 对象的"锦囊妙计"。
- 详细介绍: 定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。
- 专家视点: 它的终极目标是消灭冗长的 if/else。让你的代码更具弹性,增加新算法只需加个类。
- 应用: 购物车计算。普通会员打 9 折,金牌会员 8 折,促销满减 20。每个折扣逻辑都是一个策略类。
7.1. 场景:商场促销折扣
假设你正在开发一个收银系统,不同的节日有不同的打折算法。
不使用策略模式(Hardcode)
你会写出大量的 if-else 或 switch,每增加一种促销手段都要修改核心逻辑。
csharp
public double GetPrice(string type, double price)
{
if (type == "满减") return price - 20;
if (type == "打折") return price * 0.8;
return price;
}
使用策略模式
我们将每种算法抽象成一个独立的类。
csharp
// 1. 策略接口 (Strategy)
public interface IDiscountStrategy
{
double Calculate(double price);
}
// 2. 具体策略 (Concrete Strategies)
public class CashbackStrategy : IDiscountStrategy
{
public double Calculate(double price) => price >= 100 ? price - 20 : price;
}
public class RebateStrategy : IDiscountStrategy
{
public double Calculate(double price) => price * 0.8;
}
// 3. 上下文环境 (Context) - 持有策略的引用
public class OrderContext
{
private IDiscountStrategy _strategy;
public void SetStrategy(IDiscountStrategy strategy) => _strategy = strategy;
public double GetFinalPrice(double price) => _strategy.Calculate(price);
}
- 典型场景:UI 按钮点击、异步任务完成通知、价格波动告警。它让"动作触发者"和"动作响应者"完全互不认识,但配合默契。

7.2. 类图

7.3. 关键概念
- 组合优于继承 (Composition over Inheritance): 策略模式通过在
Context中持有接口引用,实现了在运行时动态更换算法的能力,而不是在编译时死锁。 - 开闭原则 (Open-Closed Principle): 当需要新增一种"拼团折扣"时,你只需要新建一个实现类,完全不需要动
OrderContext的代码。 - 消除条件分支: 它是消除代码中复杂
if-else或switch-case的良药。
7.4. 策略模式 vs 工厂模式
这两个模式经常配合使用,但职责不同:
| 模式 | 关注点 | 角色 |
|---|---|---|
| 工厂模式 (Factory) | 创建对象 | 负责"生出"合适的策略实例。 |
| 策略模式 (Strategy) | 执行行为 | 负责"执行"被选中的算法逻辑。 |
实战用法: 通常我们会用 工厂模式 根据配置文件生产出一个 Strategy,然后丢给 Context 去执行。
7.5. 轻量级策略
在现代 C# 中,如果策略逻辑非常简单,我们往往不写那么多类,而是直接用 委托 (Delegate) 或 Lambda 表达式。
csharp
// 这里的 Func<double, double> 本质上就是一个匿名策略
public void ProcessOrder(double price, Func<double, double> strategy)
{
double finalPrice = strategy(price);
Console.WriteLine($"最终价格:{finalPrice}");
}
// 调用时直接传逻辑:
ProcessOrder(100, p => p * 0.8);
4. 模式选择流程图

| 模式 | 核心痛点 | .NET 典型应用 | 专家建议 |
|---|---|---|---|
| 单例 | 全局状态混乱 | HttpContext.Current, 日志类 | 优先用 DI 注入单例,少用 static Instance。 |
| 工厂 | switch/case 满天飞 | HttpClientFactory, 数据库驱动创建 | 配合反射或 DI 容器可以消除最后的 switch。 |
| DI | 类与类之间强耦合 | ASP.NET Core 全家桶 | 接口先行,不要针对具体实现类编程。 |
| 观察者 | 硬编码的联动逻辑 | event 关键字, IObservable | 记得取消订阅(-=),否则会引发内存泄漏! |
5. 示例
在实际的 .NET 生产环境中,工厂模式和**依赖注入(DI)**通常是"合体"使用的。
为了让你在项目中能直接"复制粘贴"这种高级感,我用第一性原理拆解一个最经典的实战场景:多渠道支付系统(微信、支付宝、银联)。
DI + 工厂模式
如果不用设计模式,你的代码里全是 if (type == "Ali")。用了这套方案,你的 Controller 只需要管业务逻辑,具体的创建过程交给工厂。
1. 定义接口(抽象)
csharp
public interface IPaymentService
{
string ChannelName { get; }
void Pay(decimal amount);
}
2. 具体实现
csharp
public class AliPayService : IPaymentService
{
public string ChannelName => "AliPay";
public void Pay(decimal amount) => Console.WriteLine($"支付宝支付 {amount} 元");
}
public class WechatPayService : IPaymentService
{
public string ChannelName => "WechatPay";
public void Pay(decimal amount) => Console.WriteLine($"微信支付 {amount} 元");
}
3. 智能工厂(配合 DI 容器)
这里的技巧是:直接在工厂里注入 IEnumerable<IPaymentService>,DI 容器会自动把所有实现类都给你找来。
csharp
public interface IPaymentFactory
{
IPaymentService GetPaymentService(string channel);
}
public class PaymentFactory : IPaymentFactory
{
private readonly IEnumerable<IPaymentService> _services;
public PaymentFactory(IEnumerable<IPaymentService> services)
{
_services = services;
}
public IPaymentService GetPaymentService(string channel)
{
// 根据渠道名匹配具体的实现类
var service = _services.FirstOrDefault(s => s.ChannelName == channel);
return service ?? throw new Exception("不支持该支付渠道");
}
}
4. 注册与使用
在 Program.cs 中注册,逻辑就通了。
csharp
// 注册服务
builder.Services.AddScoped<IPaymentService, AliPayService>();
builder.Services.AddScoped<IPaymentService, WechatPayService>();
builder.Services.AddSingleton<IPaymentFactory, PaymentFactory>();
// 业务调用
app.MapPost("/pay", (string channel, decimal amount, IPaymentFactory factory) =>
{
var service = factory.GetPaymentService(channel);
service.Pay(amount);
return Results.Ok();
});
这样写的好处
- 好扩展 :哪天要加"数字货币支付",你只需要新建一个类实现
IPaymentService并在Program.cs注册一下,原有的工厂代码和 Controller 代码一个字都不用改(完美的开闭原则)。 - 生命周期安全 :所有对象都由 DI 托管,不用担心手动
new出来的对象没法释放内存(解决你之前担心的using泄露隐患)。 - 单元测试友好:可以轻松地 Mock(模拟)接口,而不需要真的去调用支付网关。
单例模式中 Lazy<T> 的线程安全实现 & 观察者模式 在 .NET 中的事件模型
1. 深度单例:Lazy
在 C# 中,手动写 if(instance == null) lock(...) 已经过时了。Lazy<T> 内置了双重检查锁定(Double-Check Locking)的逻辑,且性能更优。
- 核心原理:
LazyThreadSafetyMode.ExecutionAndPublication确保只有一个线程能执行初始化。
csharp
public class GlobalConfigManager
{
// 这种写法天然支持线程安全且延迟加载
private static readonly Lazy<GlobalConfigManager> _lazy =
new Lazy<GlobalConfigManager>(() => new GlobalConfigManager());
public static GlobalConfigManager Instance => _lazy.Value;
private GlobalConfigManager()
{
// 模拟从数据库或文件读取配置,这操作很重,只执行一次最好
}
}
- 双重检查锁定 (Double-Check Locking):一种并发设计模式,旨在减少获取锁的开销。
- 线程安全模式 (LazyThreadSafetyMode) :指定
Lazy实例如何在多个线程间同步。
2. 观察者模式:事件流
在 .NET 中,我们不用去生搬硬套设计模式书上的 Observer 接口,直接用 event 关键字,它本质上就是观察者模式的语法糖。

- 避坑指南: 观察者模式最怕内存泄漏。如果"被观察者"生命周期很长,而"观察者"很短,记得在观察者销毁时"取消订阅"。
csharp
public class OrderService
{
// 定义事件(发布者)
public event Action<string> OnOrderCreated;
public void CreateOrder(string orderId)
{
Console.WriteLine($"订单 {orderId} 已创建");
// 触发通知,所有订阅了的人都会收到
OnOrderCreated?.Invoke(orderId);
}
}
// 在其他地方使用
var orderService = new OrderService();
orderService.OnOrderCreated += (id) => Console.WriteLine($"短信已发送:{id}"); // 订阅
6. 避坑
- 不要过度抽象 :如果你的业务这辈子都只有一种逻辑,直接
new也没错。模式是为了应对**"变化"**而生的。 - 单例与状态 :如果你把
AliPayService注册为单例(Singleton),记得它内部不能存有跟用户相关的状态字段,否则 A 用户的订单信息可能会串到 B 用户身上。
这就是"少加班"的秘密:用代码结构来应对需求变化,而不是用体力。
6.1. 建议
当你把这四种模式揉在一起时,你会发现你的 Program.cs 变得非常清晰:
- 单例 存放那些全局不变的配置或缓存。
- DI 帮你管理所有 Service 的"出生"与"死亡"。
- 工厂 帮你根据业务参数(如支付渠道、用户等级)选出对的 Service。
- 观察者 处理那些"顺便执行"的动作(如发邮件、记日志),让主业务逻辑不臃肿。