策略模式是oop中最著名的設計模式之一,是對方法行為的抽象,可以歸類為行為設計模式,也是oop中interface經典的應用。其特點簡單又實用,是我最喜歡的模式之一。策略模式定義了一個擁有共同行為的算法族,每個算法都被封裝起來,可以互相替換,獨立於客戶端而變化。策略模式本身的實現比較簡單,但是結合單例模式+簡單工廠模式+注解+反射,可以構造出近乎完善的策略模式,徹底的消除if-else。
一、策略模式基礎
策略模式是oop中最著名的設計模式之一,是對方法行為的抽象,可以歸類為行為設計模式,也是oop中interface經典的應用。其特點簡單又實用,是我最喜歡的模式之一。策略模式定義了一個擁有共同行為的算法族,每個算法都被封裝起來,可以互相替換,獨立於客戶端而變化。
我們可以從三個方面來理解策略模式:
- 算法族
使用多種不同的處理方式,做同樣的事情,僅僅是具體行為有差別。這些處理方式,組合構成算法策略族,它們的共性,體現在策略接口行為上。 - 算法封裝
將各個算法封裝到不同的類中,這樣有助於客戶端來選擇合適的算法。 - 可互相替換
客戶端可以在運行時選擇使用哪個算法,而且算法可以進行替換,所以客戶端依賴於策略接口。
據此,可以推斷出策略模式的使用場景:
- 針對同一問題的多種處理方式,僅僅是具體行為有差別時;
- 需要安全地封裝多種同一類型的操作時;
- 同一抽象類有多個子類,而客戶端需要使用 if-else 或者 switch-case 來選擇具體子類時。
策略模式UML類圖如下:
可以看到策略模式涉及到三個角色:
環境(Context)角色:持有一個Strategy的引用。
抽象策略(Strategy)角色:這是一個抽象角色,通常由一個接口或抽象類實現。此角色給出所有的具體策略類所需的接口。
具體策略(ConcreteStrategy)角色:包裝了相關的具體算法或行為。
策略模式典型代碼如下:

1 /// <summary> 2 /// 抽象策略類(接口) 3 /// </summary> 4 public interface Strategy 5 { 6 /// <summary> 7 /// 抽象策略方法 8 /// </summary> 9 void StrategyFunc(); 10 } 11 /// <summary> 12 /// 具體策略類A 13 /// </summary> 14 public class ConcreteStrategyA : Strategy 15 { 16 public void StrategyFunc() 17 { 18 //具體方法A 19 } 20 } 21 /// <summary> 22 /// 具體策略類B 23 /// </summary> 24 public class ConcreteStrategyB : Strategy 25 { 26 public void StrategyFunc() 27 { 28 //具體方法B 29 } 30 } 31 /// <summary> 32 /// 具體策略類C 33 /// </summary> 34 public class ConcreteStrategyC : Strategy 35 { 36 public void StrategyFunc() 37 { 38 //具體方法C 39 } 40 } 41 /// <summary> 42 /// 環境 43 /// </summary> 44 public class Context 45 { 46 //持有一個具體策略的對象 47 private Strategy _strategy; 48 /// <summary> 49 /// 構造函數,傳入一個具體策略對象 50 /// </summary> 51 /// <param name="strategy"></param> 52 public Context(Strategy strategy) 53 { 54 this._strategy = strategy; 55 } 56 /// <summary> 57 /// 調用策略方法 58 /// </summary> 59 public void ContextFunc() 60 { 61 _strategy.StrategyFunc(); 62 } 63 } 64 /// <summary> 65 /// 客戶端調用 66 /// </summary> 67 public class Client 68 { 69 public void Main(string param) 70 { 71 Context context; 72 if (param == "A") 73 { 74 context = new Context(new ConcreteStrategyA()); 75 } 76 else if(param == "B") 77 { 78 context = new Context(new ConcreteStrategyB()); 79 } 80 else if(param == "C") 81 { 82 context = new Context(new ConcreteStrategyC()); 83 } 84 else 85 { 86 throw new Exception("沒有可用的策略"); 87 } 88 context.ContextFunc(); 89 } 90 }
二、策略模式實例-會員策略
假設某店家推出三種會員,分別為普通會員,金牌會員和鑽石會員,還有就是普通顧客,針對不同的會員顧客,購物結算時有不同的打折方式。購物后,客戶的歷史購物金額累計,可以自動升級到相應的會員級別。
這里的購物結算時,我們就很適宜使用策略模式,因為策略模式描述的就是算法的不同。這里舉例我們就采用簡單的方式,四類顧客分別采用原價(非會員普通顧客),九折、八折、七折的收錢方式。
那么我們首先要有一個計算價格的策略接口:
1 /// <summary> 2 /// Vip算法規則 3 /// </summary> 4 public interface IVipAlgorithm 5 { 6 int CalcPrice(int originalPrice); 7 }
然后下面是不同會員顧客的收錢算法的具體實現:
1 public enum Vip 2 { 3 普通會員 = 1, 4 黃金會員 = 2, 5 鑽石會員 = 3 6 }
1 /// <summary> 2 /// 非會員顧客 3 /// </summary> 4 public class VipNone : IVipAlgorithm 5 { 6 public int CalcPrice(int originalPrice) 7 { 8 return originalPrice; 9 } 10 }
1 /// <summary> 2 /// 普通會員 3 /// </summary> 4 public class VipOrdinary : IVipAlgorithm 5 { 6 public int CalcPrice(int orginalPrice) 7 { 8 var currentPrice = (int)(orginalPrice * 0.9 + 0.5); 9 return currentPrice; 10 } 11 }
1 /// <summary> 2 /// 金牌會員 3 /// </summary> 4 public class VipGold : IVipAlgorithm 5 { 6 public int CalcPrice(int orginalPrice) 7 { 8 var currentPrice = (int)(orginalPrice * 0.8 + 0.5); 9 return currentPrice; 10 } 11 }
1 /// <summary> 2 /// 鑽石會員 3 /// </summary> 4 public class VipDiamond: IVipAlgorithm 5 { 6 public int CalcPrice(int orginalPrice) 7 { 8 var currentPrice = (int)(orginalPrice * 0.7 + 0.5); 9 return currentPrice; 10 } 11 }
接下來看顧客類,顧客類中自動累計歷史購物金額,判斷會員級別,進行打折結算。
1 public class Customer 2 { 3 public int _totalAmount = 0; 4 public Vip? _vip = null; 5 public IVipAlgorithm _vipAlgorithm; 6 public void Buy(decimal originPriceM) 7 { 8 if (_totalAmount >= 5000 * 100) 9 { 10 _vip = Vip.鑽石會員; 11 _vipAlgorithm = new VipDiamond(); 12 } 13 else if (_totalAmount >= 3000 * 100) 14 { 15 _vip = Vip.黃金會員; 16 _vipAlgorithm = new VipGold(); 17 } 18 else if (_totalAmount >= 1000 * 100) 19 { 20 _vip = Vip.普通會員; 21 _vipAlgorithm = new VipOrdinary(); 22 } 23 else 24 { 25 _vip = null; 26 _vipAlgorithm = new VipNone(); 27 } 28 var originPrice = (int)originPriceM * 100; 29 var finalPrice = _vipAlgorithm.CalcPrice(originPrice); 30 //打印 31 Console.WriteLine($"您在本店歷史消費總額:{_totalAmount * 0.01}元"); 32 var vipMsg = _vip.HasValue ? $"您是本店會員:{_vip.Value.ToString()}" : "您未升級為本店會員"; 33 Console.WriteLine(vipMsg); 34 Console.WriteLine($"本次購買商品原價{originPrice * 0.01}元,需支付{finalPrice * 0.01}元"); 35 _totalAmount += originPrice; 36 Console.WriteLine(); 37 } 38 }
最后是客戶端調用,系統會幫我們自動調整結算打折策略。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Customer cust = new Customer(); 6 cust.Buy(1000); 7 cust.Buy(1000); 8 cust.Buy(1000); 9 cust.Buy(1000); 10 cust.Buy(1000); 11 cust.Buy(1000); 12 cust.Buy(1000); 13 Console.ReadLine(); 14 } 15 }
輸出結果:
可以看到隨着此顧客購物金額累加,會員身份自動升級,購物結算折扣也越來越多。這樣設計的好處顯而易見,客戶端不再依賴於具體的收費策略,依賴於抽象永遠是正確的。
在上面的基礎上,我們可以使用簡單工廠來解耦顧客類的結算策略依賴。
1 public class VipAlgorithmFactory 2 { 3 private VipAlgorithmFactory() { } 4 //根據客戶的總金額產生相應的策略 5 public static IVipAlgorithm GetVipAlgorithm(Customer cust, out Vip? vipLevel) 6 { 7 if (cust._totalAmount >= 5000 * 100) 8 { 9 vipLevel = Vip.鑽石會員; 10 return new VipDiamond(); 11 } 12 else if (cust._totalAmount >= 3000 * 100) 13 { 14 vipLevel = Vip.黃金會員; 15 return new VipGold(); 16 } 17 else if (cust._totalAmount >= 1000 * 100) 18 { 19 vipLevel = Vip.普通會員; 20 return new VipOrdinary(); 21 } 22 else 23 { 24 vipLevel = null; 25 return new VipNone(); 26 } 27 } 28 }
這樣就將制定策略的功能從客戶類分離出來,我們的客戶類可以變成這樣。
1 public class Customer 2 { 3 public int _totalAmount = 0; 4 public Vip? _vip = null; 5 public IVipAlgorithm _vipAlgorithm; 6 public void Buy(decimal originPriceM) 7 { 8 //變化點,我們將策略的制定轉移給了策略工廠,將這部分責任分離出去 9 _vipAlgorithm = VipAlgorithmFactory.GetVipAlgorithm(this,out this._vip); 10 var originPrice = (int)originPriceM * 100; 11 var finalPrice = _vipAlgorithm.CalcPrice(originPrice); 12 // 13 Console.WriteLine($"您在本店歷史消費總額:{_totalAmount * 0.01}元"); 14 var vipMsg = _vip.HasValue ? $"您是本店會員:{_vip.Value.ToString()}" : "您未升級為本店會員"; 15 Console.WriteLine(vipMsg); 16 Console.WriteLine($"本次購買商品原價{originPrice * 0.01}元,需支付{finalPrice * 0.01}元"); 17 _totalAmount += originPrice; 18 Console.WriteLine(); 19 } 20 }
三、消除If-else
雖然結合簡單工廠模式,我們的策略模式靈活了一些,但不免發現在工廠中仍然存在if-else判斷,如果當我們增加一個會員級別,又得增加一個if-else語句,這是簡單工廠的缺點,對修改開放。
那有什么方法,可以較好的解決這個問題呢?那就是使用注解,我們給策略類加上Vip級別注解,以根據Vip級別確定哪個策略生效,從而解決結算折扣策略選擇的問題。另外在較復雜的系統中,我們還可以考慮使用IOC容器和依賴注入的方式來解決。這里只演示注解方式。
首先添加一個注解類,然后再為我們的每個策略類增加注解,如為鑽石會員策略類增加注解[Vip(Vip.鑽石會員)]。
1 public class VipAttribute : Attribute 2 { 3 public Vip Vip { get; set; } 4 public VipAttribute(Vip vip) 5 { 6 Vip = vip; 7 } 8 }
然后增加一個配置查詢類,使用單例模式,避免每次訪問都去實例化配置。
1 public class VipConfig 2 { 3 public readonly Dictionary<Vip, int> VipCondition; 4 public readonly Dictionary<Vip, IVipAlgorithm> VipAlgorithm; 5 private static VipConfig _vipConfigInstance; 6 private VipConfig() 7 { 8 //這里將配置硬編碼到字典中,實際可以從配置文件中讀取 9 VipCondition = new Dictionary<Vip, int> { { Vip.普通會員, 1000 * 100 }, { Vip.黃金會員, 3000 * 100 }, { Vip.鑽石會員, 5000 * 100 } }; 10 VipAlgorithm = Assembly.GetExecutingAssembly().GetExportedTypes() 11 .Select(t => new 12 { 13 Type = t, 14 Vip = t.GetCustomAttribute<VipAttribute>()?.Vip 15 }) 16 .Where(x => x.Vip != null) 17 .ToDictionary(x => x.Vip.Value, x => (IVipAlgorithm)Activator.CreateInstance(x.Type)); 18 } 19 public static VipConfig Instance 20 { 21 get 22 { 23 if (_vipConfigInstance == null) 24 { 25 _vipConfigInstance = new VipConfig(); 26 } 27 return _vipConfigInstance; 28 } 29 } 30 }
策略工廠仍然幫我們自動找到適應的策略,現在將我們的工廠類調整如下:
1 public class VipAlgorithmFactory 2 { 3 public static IVipAlgorithm GetVipAlgorithm(Customer cust, out Vip? vipLevel) 4 { 5 var custVip = VipConfig.Instance.VipCondition.Where(x => x.Value <= cust._totalAmount) 6 .OrderByDescending(x => x.Value) 7 .ToList(); 8 IVipAlgorithm vipAlgorithm = null; 9 if (custVip.Count == 0) 10 { 11 vipLevel = null; 12 vipAlgorithm = new VipNone(); 13 } 14 else 15 { 16 vipLevel = custVip.First().Key; 17 vipAlgorithm = VipConfig.Instance.VipAlgorithm[vipLevel.Value]; 18 } 19 return vipAlgorithm; 20 } 21 }
最終的顧客類相應調整如下:
1 public class Customer 2 { 3 public int _totalAmount = 0; 4 public Vip? _vip = null; 5 public IVipAlgorithm _vipAlgorithm; 6 public void Buy(decimal originPriceM) 7 { 8 _vipAlgorithm = VipAlgorithmFactory.GetVipAlgorithm(this, out this._vip); 9 var originPrice = (int)originPriceM * 100; 10 var finalPrice = _vipAlgorithm.CalcPrice(originPrice); 11 // 12 Console.WriteLine($"您在本店歷史消費總額:{_totalAmount * 0.01}元"); 13 var vipMsg = _vip.HasValue ? $"您是本店會員:{_vip.Value.ToString()}" : "您未升級為本店會員"; 14 Console.WriteLine(vipMsg); 15 Console.WriteLine($"本次購買商品原價{originPrice * 0.01}元,需支付{finalPrice * 0.01}元"); 16 _totalAmount += originPrice; 17 Console.WriteLine(); 18 } 19 }
最終,通過單例、工廠、注解、反射共同配合,配置讀取單例化,實現了更加完善的策略模式,消除了if-else。
四、總結
一,策略模式在.NET中的應用
在.NET Framework中也不乏策略模式的應用例子。例如,在.NET中,為集合類型ArrayList和List<T>提供的排序功能,其中實現就利用了策略模式,定義了IComparer接口來對比較算法進行封裝,實現IComparer接口的類可以是順序,或逆序地比較兩個對象的大小,還可以使用快速排序,希爾排序,歸並排序類等。具體.NET中的實現可以使用反編譯工具查看List<T>.Sort(IComparer<T>)的實現。其中List<T>就是承擔着環境角色,而IComparer<T>接口承擔着抽象策略角色,具體的策略角色就是實現了IComparer<T>接口的類,List<T>類本身存在實現了該接口的類,我們可以自定義繼承於該接口的具體策略類。
二,策略模式的優缺點
優點
- 易於擴展,增加一個新的策略只需要添加一個具體的策略類即可,基本不需要改變原有的代碼,符合開放封閉原則
- 避免使用多重條件選擇語句,充分體現面向對象設計思想
- 策略類之間可以自由切換,由於策略類都實現同一個接口,所以使它們之間可以自由切換
- 每個策略類使用一個策略類,符合單一職責原則
- 客戶端與策略算法解耦,兩者都依賴於抽象策略接口,符合依賴反轉原則
- 客戶端不需要知道都有哪些策略類,符合最小知識原則
缺點
- 策略模式,當策略算法太多時,會造成很多的策略類
- 客戶端不知道有哪些策略類,不能決定使用哪個策略類,這點可以通過本文中的方式解決,也可以考慮使用IOC容器和依賴注入的方式來解決
三,結論
策略模式、狀態模式、責任鏈模式都是在解決if-else的問題,但目的不太一樣。策略模式主要是對方法的封裝,把一系列方法封裝到一系列的策略類中,客戶端可以根據不同情況選擇使用適宜的策略類,不同的策略類可以自由切換。
策略模式除了在OOP中實踐,也可以在FP中應用。OOP讓我們以抽象設計的角度看系統,而FP讓我們以簡化設計的角度看系統,我們建議以OOP做第一階段的重構,再輔以FP做第二階段的重構,可以解決OOP容易過度設計的問題。在這里即可以進一步解決策略類過多的問題,比如策略接口可以退化為一個委托,或如Func<T1,T2>,C#向FP邁進,都是起源於委托的。然后策略類通常只有一個方法,所以真的有必要建立這么多類嗎?完全可以退化為一個靜態類加多個靜態策略方法的形式,還有工廠類則從返回策略類實例變化為返回策略方法,這就是輔以FP思想重構C#策略模式的思路。
C# 1.0主要是OOP,C# 2.0主要是泛型,C# 3.0之后主要是FP部分,如Lambda、Linq,C# 也是在3.0開始與Java分道揚鑣,朝向OOP+FP雙hybrid語言目標推進,尤其到C# 7.0非常明顯,如Tuple、Descontruction、Pattern Matching、Local Function...都是FP語言基本的機制。
完整的示例代碼已放置於 https://github.com/gitsongs/StrategyPattern
--End