- 什么是策略模式? (定义与意图)
- 为什么要使用策略模式? (解决的问题与优势)
- 策略模式的结构与参与者 (UML类图与角色讲解)
- C#代码实现示例 (一个生动且完整的例子)
- 策略模式的优缺点 (全面客观地评价)
- 在C#中的实际应用场景
- 策略模式与其他模式的区别 (避免混淆)
1. 什么是策略模式?
策略模式是一种行为型设计模式。它的核心思想是:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。
简单来说,策略模式让算法的变化独立于使用算法的客户。这意味着,当需要改变或增加一种算法时,你不需要修改使用算法的客户端代码,只需要增加一个新的策略实现即可。
一个生活中的比喻:
假设你要去旅游,你可以选择不同的交通方式:坐飞机、坐火车、或者自己开车。
- “去旅游” 是你的上下文。
- “交通方式” 是你的策略。
- “飞机”、“火车”、“汽车” 是具体策略。
你可以根据预算、时间等因素,随时更换你的交通方式(策略),但“去旅游”这个目标(上下文)是不变的。
2. 为什么要使用策略模式?
想象一个没有策略模式的场景:你有一个ShoppingCart(购物车)类,它需要根据不同的用户类型计算折扣。
// 糟糕的设计:违反了开闭原则
public class ShoppingCart
{
public decimal CalculatePrice(decimal originalPrice, string userType)
{
switch (userType)
{
case "Normal":
return originalPrice;
case "VIP":
return originalPrice * 0.8m;
case "SuperVIP":
return originalPrice * 0.5m;
default:
return originalPrice;
}
}
}这种设计的问题:
- 违反开闭原则:每当需要增加一种新的用户类型(比如
EmployeeVIP),你就必须修改ShoppingCart类的CalculatePrice方法。这增加了引入bug的风险,也使得代码难以维护。 - 代码臃肿:所有的折扣逻辑都堆积在一个类里,如果计算逻辑变得复杂,这个方法会变得异常庞大和难以阅读。
- 算法与上下文耦合:折扣算法和购物车本身紧密地绑定在一起。
策略模式如何解决这个问题?
策略模式将“计算折扣”这个行为从ShoppingCart类中抽离出来,封装成独立的策略类。ShoppingCart类只需要持有一个策略的引用,并在需要时调用它即可。
3. 策略模式的结构与参与者
策略模式通常包含三个核心角色:
IStrategy(策略接口/抽象类)- 定义了一个所有具体策略都必须实现的公共接口。这个接口就是上下文用来调用算法的“契约”。
ConcreteStrategy(具体策略类)- 实现了
IStrategy接口,提供了具体的算法实现。例如:NormalDiscount,VIPDiscount,SuperVIPDiscount。
- 实现了
Context(上下文类)- 持有一个对
IStrategy对象的引用。 - 它可以定义一个接口来让
Strategy访问它的数据。 - 它的客户端通常在创建时或运行时向其传递一个具体的
Strategy对象。 - 它不关心具体是哪个策略,只负责调用策略接口定义的方法。
- 持有一个对
UML 类图:
+----------------+ +------------------+
| Context |------>| IStrategy |
+----------------+ +------------------+
| - strategy | | + Execute() |
+----------------+ +------------------+
| + SetStrategy()| ^
| + DoSomething()| |
+----------------+ +-------+-------+
| ConcreteStrategyA |
+-------------------+
| + Execute() |
+-------------------+
| ConcreteStrategyB |
+-------------------+
| + Execute() |
+-------------------+4. C#代码实现示例
我们用上面购物车折扣的例子来完整实现策略模式。
步骤 1: 定义策略接口 IDiscountStrategy
这个接口定义了所有折扣策略必须遵守的契约。
// IStrategy: 策略接口
public interface IDiscountStrategy
{
// 计算折扣后的价格
decimal CalculateDiscount(decimal originalPrice);
}步骤 2: 创建具体策略类
为每种折扣规则创建一个独立的类。
// ConcreteStrategyA: 普通用户无折扣
public class NormalDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal originalPrice)
{
Console.WriteLine("使用普通用户折扣:无折扣");
return originalPrice;
}
}
// ConcreteStrategyB: VIP用户8折
public class VipDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal originalPrice)
{
Console.WriteLine("使用VIP用户折扣:8折");
return originalPrice * 0.8m;
}
}
// ConcreteStrategyC: 超级VIP用户5折
public class SuperVipDiscountStrategy : IDiscountStrategy
{
public decimal CalculateDiscount(decimal originalPrice)
{
Console.WriteLine("使用超级VIP用户折扣:5折");
return originalPrice * 0.5m;
}
}步骤 3: 创建上下文类 ShoppingCart
上下文类持有一个策略对象,并利用它来执行计算。
// Context: 上下文类
public class ShoppingCart
{
// 持有一个策略接口的引用
private IDiscountStrategy _discountStrategy;
// 构造函数中注入策略
public ShoppingCart(IDiscountStrategy discountStrategy)
{
_discountStrategy = discountStrategy;
}
// 允许在运行时更换策略
public void SetDiscountStrategy(IDiscountStrategy discountStrategy)
{
_discountStrategy = discountStrategy;
}
// 上下文的方法,它会将具体工作委托给策略
public decimal Checkout(decimal originalPrice)
{
// ... 其他购物车逻辑,如计算总价、运费等 ...
// 委托给策略对象来计算折扣
return _discountStrategy.CalculateDiscount(originalPrice);
}
}步骤 4: 客户端代码
客户端负责创建具体的策略对象,并将其传递给上下文。
public class Program
{
public static void Main(string[] args)
{
decimal originalPrice = 1000m;
// 场景1:一个普通用户结账
Console.WriteLine("--- 普通用户结账 ---");
var normalStrategy = new NormalDiscountStrategy();
var cart1 = new ShoppingCart(normalStrategy);
decimal finalPrice1 = cart1.Checkout(originalPrice);
Console.WriteLine($"最终价格: {finalPrice1:C}\n");
// 场景2:一个VIP用户结账
Console.WriteLine("--- VIP用户结账 ---");
var vipStrategy = new VipDiscountStrategy();
var cart2 = new ShoppingCart(vipStrategy);
decimal finalPrice2 = cart2.Checkout(originalPrice);
Console.WriteLine($"最终价格: {finalPrice2:C}\n");
// 场景3:用户在结账前突然升级为超级VIP
Console.WriteLine("--- 用户升级为超级VIP后结账 ---");
var superVipStrategy = new SuperVipDiscountStrategy();
// 运行时动态切换策略
cart2.SetDiscountStrategy(superVipStrategy);
decimal finalPrice3 = cart2.Checkout(originalPrice);
Console.WriteLine($"最终价格: {finalPrice3:C}\n");
}
}输出结果:
--- 普通用户结账 ---
使用普通用户折扣:无折扣
最终价格: ¥1,000.00
--- VIP用户结账 ---
使用VIP用户折扣:8折
最终价格: ¥800.00
--- 用户升级为超级VIP后结账 ---
使用超级VIP用户折扣:5折
最终价格: ¥500.00这个例子完美地展示了策略模式的威力:算法(折扣逻辑)与使用算法的上下文(购物车)解耦,并且可以在运行时灵活切换。
5. 策略模式的优缺点
优点:
- 符合开闭原则:你可以在不修改现有代码(上下文类)的情况下,引入新的策略(只需添加新的策略类)。
- 避免使用多重条件语句:将
switch-case或if-else链消除,使代码更清晰、更易于维护。 - 算法可以自由切换:由于策略实现了同一接口,它们之间可以互相替换。
- 扩展性良好:增加新策略非常容易,符合高内聚、低耦合的设计原则。
- 提高了代码的复用性:每个策略都是一个独立的类,可以在其他需要相同算法的地方复用。
缺点:
- 策略类数量会增加:每增加一个策略,就要增加一个新的类。当策略很多时,可能会导致类的数量爆炸。
- 所有策略类都需要对外暴露:客户端必须知道所有的策略类,并自行决定使用哪一个。这增加了客户端的复杂度。
- 策略之间可能存在共享数据:如果不同策略之间需要共享一些状态,那么就需要在上下文中进行管理,这会增加上下文的复杂性。
6. 在C#中的实际应用场景
策略模式在C#开发中非常常见,很多框架和库都隐式或显式地使用了它。
- ASP.NET Core 中间件管道:你可以决定使用哪些认证中间件(如Cookie认证、JWT Bearer认证、OpenID Connect认证),每种认证方式就是一种策略。
- 依赖注入:DI容器本身就是一个巨大的策略工厂。当你为一个接口注册不同的实现时,你就是在配置使用哪种“策略”。
- 排序算法:
Array.Sort()或List.Sort()方法可以接受一个IComparer接口的实现。IComparer就是一个策略接口,你可以提供不同的比较策略来定义自定义排序。 - 数据验证:在ASP.NET Core中,你可以为同一个模型属性添加多个验证特性(如
[Required],[StringLength],[RegularExpression]),每个验证特性都是一个独立的验证策略。 - 日志记录:你可以配置不同的日志提供程序(如Console、Debug、File、Azure App Service),每个提供程序就是一种记录日志的策略。
7. 策略模式与其他模式的区别
策略模式 vs. 状态模式
- 意图不同:策略模式是为了让算法可以互换;状态模式是为了让一个对象在其内部状态改变时,改变其行为。
- 关系不同:策略模式中,客户端通常主动选择并设置策略;状态模式中,状态对象会自行在上下文中切换。
- 耦合度:策略模式中,策略与上下文通常是独立的;状态模式中,状态对象之间常常有关联和转换。
策略模式 vs. 模板方法模式
- 继承 vs. 组合:模板方法模式基于继承,它在父类中定义算法骨架,子类实现具体步骤;策略模式基于组合,它将整个算法封装在对象中,并让上下文持有该对象。
- 改变点不同:模板方法改变的是算法的局部步骤;策略模式改变的是整个算法。
总结
策略模式是一个非常基础且强大的设计模式,它通过封装变化,将算法的调用者和实现者解耦。当你遇到一系列具有相似行为但实现方式不同的场景时,特别是当你需要避免使用大量的if-else或switch-case语句时,策略模式是一个非常值得考虑的优秀解决方案。它使你的代码更加灵活、可扩展和易于维护。