写在前面
上一篇讲 SOLID 的开闭原则时埋了个雷:折扣逻辑别写成一坨 if/switch。怎么改?答案就是策略模式。
这是后端开发用得最多的模式,没有之一。它解决的问题极常见:同一个动作有多种做法,需要按情况选一个。
一、问题:一坨 switch
1
2
3
4
5
6
7
8
9
10
11
12
| // 反例:折扣计算,每加一种活动就往这里塞
public decimal CalculateDiscount(string type, decimal price)
{
return type switch
{
"Normal" => price, // 不打折
"Vip" => price * 0.8m, // VIP 八折
"Coupon" => price - 50m, // 满减券
"Employee" => price * 0.5m, // 员工五折
_ => price
};
}
|
1
2
3
4
5
6
7
| 这坨代码的病:
✗ 违反开闭原则:加"双11满3件打7折"要改这个方法(动老代码)
✗ 越长越难维护,分支互相影响
✗ 算法全堆一处,没法单独复用/测试
✗ DiscountService 依赖了所有折扣细节
病根:把"选择哪个算法"和"算法本身"搅在一起了。
|
二、策略模式:把算法拆开
1
2
3
4
5
6
7
8
9
10
11
12
| 意图:定义一系列算法,各自封装,可互相替换。
算法的变化不影响使用它的客户端。
结构:
IStrategy ← 算法的抽象接口
ConcreteStrategy ← 每个算法一个实现
Context ← 持有某个策略,把工作委托给它
好处:
✓ 新增算法 = 加一个类,老代码不动(开闭)
✓ 算法可单独测试、复用
✓ 客户端不关心算法细节,只管"调它"
|
三、经典 OOP 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // 1. 策略接口
public interface IDiscountStrategy
{
decimal Apply(decimal price);
}
// 2. 每个算法一个实现
public class NormalDiscount : IDiscountStrategy
{
public decimal Apply(decimal price) => price;
}
public class VipDiscount : IDiscountStrategy
{
public decimal Apply(decimal price) => price * 0.8m;
}
public class EmployeeDiscount : IDiscountStrategy
{
public decimal Apply(decimal price) => price * 0.5m;
}
// 3. Context:把"算折扣"委托给注入进来的策略
public class PricingService(IDiscountStrategy _discount)
{
public decimal GetFinalPrice(decimal price) => _discount.Apply(price);
}
|
1
2
| 现在加"双11折扣":写一个 DoubleElevenDiscount 类,
PricingService 一行不用改 → 开闭原则达成。
|
四、现代 C#:不必每次都建类层次
很多策略其实就是"一段逻辑",用 Func<decimal, decimal> 或 delegate 更轻:
1
2
3
4
5
6
7
8
9
10
11
12
| // 策略就是一个函数
public class PricingService
{
private readonly Func<decimal, decimal> _discount;
public PricingService(Func<decimal, decimal> discount) => _discount = discount;
public decimal GetFinalPrice(decimal price) => _discount(price);
}
// 注册时直接传 lambda(配合 DI)
services.AddTransient<PricingService>(_ => new PricingService(p => p * 0.8m)); // VIP
|
多策略按 key 选——经典的"策略 + 工厂"组合,用字典派发最干净:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class DiscountCalculator
{
// 字典:type → 算法函数
private readonly Dictionary<string, Func<decimal, decimal>> _strategies = new()
{
["Normal"] = p => p,
["Vip"] = p => p * 0.8m,
["Coupon"] = p => Math.Max(0, p - 50m),
["Employee"] = p => p * 0.5m,
};
public decimal Calculate(string type, decimal price)
=> _strategies.TryGetValue(type, out var fn) ? fn(price) : price;
}
|
1
2
3
4
5
6
7
| 对比开头的 switch:
- 加新折扣 = 字典加一行,方法体不动
- 算法集中、清晰、可测
- 没有多余的类层次,但拿到了策略模式的所有好处
这就是 .NET 里策略模式的现代写法——
"意图"没变(算法族可替换),"实现"从类层次简化成函数。
|
五、.NET 里策略无处不在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 你天天在用策略模式,只是没意识到:
LINQ:
list.OrderBy(x => x.Name) // x => x.Name 就是一个"排序策略"
list.Where(x => x.Active) // 过滤策略
→ 同样的 OrderBy,传不同 keySelector = 不同策略,框架替你 dispatch
集合比较:
new SortedSet<int>(Comparer<int>.Create((a, b) => b - a)) // 降序比较策略
ASP.NET Core:
config.AddJsonFile() / .AddEnvironmentVariables() // 每个"配置源"都是策略
serializer options:不同的命名策略、枚举转换策略
Task.Run / Parallel.ForEach 的并行度策略、
EF Core 的值比较器(IEqualityComparer<T>)……
本质都是:把"可替换的一段逻辑"作为参数传进去。
|
六、经典 OOP 形态 vs 现代函数形态,怎么选
1
2
3
4
5
6
7
8
9
10
11
12
13
| 用类(IDiscountStrategy 接口 + 实现):
✓ 算法复杂、有状态、需要注入依赖(VIP 折扣要查用户等级)
✓ 多处复用、需要独立测试某个算法
✓ 团队习惯显式类型
用函数(Func / delegate / 字典):
✓ 算法简单、无状态、就几行
✓ 想轻量,不想为一小段逻辑建一堆类
✓ 动态组合(字典派发、运行时拼装)
经验:简单用函数,复杂用类。
别为了"显得正规"给三行 lambda 套个接口。
(第一条讲的 YAGNI)
|
七、何时用 / 何时别用
1
2
3
4
5
6
7
8
9
10
11
12
| 该用策略模式:
✓ 一组同类算法,按条件选一个(折扣/支付/排序/校验…)
✓ 算法会随业务不断新增
✓ 想把选择逻辑和算法实现分开
别用:
✗ 只有一两种情况,if/switch 清清楚楚 → 别过度设计
✗ 分支不是"选算法",而是"按数据走不同流程" → 那是状态/责任链的活
✗ 算法之间共享大量状态、关系紧密 → 硬拆成策略反而更乱
信号:当 switch 分支多到让你皱眉、新增分支让你害怕,
就是策略模式该上场的时候。
|
八、小结
- 问题:一坨 switch 把"选算法"和"算法本身"搅一起,违反开闭
- 策略模式:算法各自封装、可替换,客户端只依赖抽象
- 经典实现:
IStrategy 接口 + 多实现 + Context 委托 - 现代 C#:简单策略用
Func/delegate/字典派发,不必建类层次 - .NET 里随处可见:LINQ 的
OrderBy/Where、配置源、比较器都是策略 - 选择:算法简单用函数,复杂/有状态用类
- 信号:switch 开始让你皱眉,就上策略
下一篇讲工厂模式——创建对象的烦恼,以及 .NET DI 容器怎么把"工厂"这件事彻底接管。