设计模式实战(二):策略模式——干掉那一坨 if/switch

写在前面

上一篇讲 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 容器怎么把"工厂"这件事彻底接管。