設計完美的策略模式,消除If-else


策略模式是oop中最著名的設計模式之一,是對方法行為的抽象,可以歸類為行為設計模式,也是oop中interface經典的應用。其特點簡單又實用,是我最喜歡的模式之一。策略模式定義了一個擁有共同行為的算法族,每個算法都被封裝起來,可以互相替換,獨立於客戶端而變化。策略模式本身的實現比較簡單,但是結合單例模式+簡單工廠模式+注解+反射,可以構造出近乎完善的策略模式,徹底的消除if-else。

一、策略模式基礎

策略模式是oop中最著名的設計模式之一,是對方法行為的抽象,可以歸類為行為設計模式,也是oop中interface經典的應用。其特點簡單又實用,是我最喜歡的模式之一。策略模式定義了一個擁有共同行為的算法族,每個算法都被封裝起來,可以互相替換,獨立於客戶端而變化。

我們可以從三個方面來理解策略模式:

  1. 算法族
    使用多種不同的處理方式,做同樣的事情,僅僅是具體行為有差別。這些處理方式,組合構成算法策略族,它們的共性,體現在策略接口行為上。
  2. 算法封裝
    將各個算法封裝到不同的類中,這樣有助於客戶端來選擇合適的算法。
  3. 可互相替換
    客戶端可以在運行時選擇使用哪個算法,而且算法可以進行替換,所以客戶端依賴於策略接口。

據此,可以推斷出策略模式的使用場景:

  1. 針對同一問題的多種處理方式,僅僅是具體行為有差別時;
  2. 需要安全地封裝多種同一類型的操作時;
  3. 同一抽象類有多個子類,而客戶端需要使用 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     }
View Code

 二、策略模式實例-會員策略

假設某店家推出三種會員,分別為普通會員,金牌會員和鑽石會員,還有就是普通顧客,針對不同的會員顧客,購物結算時有不同的打折方式。購物后,客戶的歷史購物金額累計,可以自動升級到相應的會員級別。

這里的購物結算時,我們就很適宜使用策略模式,因為策略模式描述的就是算法的不同。這里舉例我們就采用簡單的方式,四類顧客分別采用原價(非會員普通顧客),九折、八折、七折的收錢方式。

那么我們首先要有一個計算價格的策略接口:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM