【Python學習日記】B站小甲魚:永久儲存(pickle模組)和異常處理(exception)
示例
策略模式是我們工作中比較常用的一個設計模式,但是初次理解起來可能會有點困難,因此我們還是先看一個例子,假設現在需要開發一個畫圖工具,畫圖工具中有鋼筆,筆刷和油漆桶,其中,鋼筆可以用於描邊,但不能填充,筆刷既可以描邊,也可以填充,油漆桶只能用於填充,但不能描邊。
看到這個需求,最容易想到的可能就是通過繼承方式實現了,即鋼筆,筆刷和油漆桶都繼承自畫圖工具,然後都實現描邊和填充功能,只不過鋼筆的填充方法什麼都不做,油漆桶的描邊方法也什麼都不做。(該部分在設計原則中有示例程式碼,是當時的一個遺留問題)
但是仔細一想,好像哪裡不對勁,因為鋼筆和油漆桶部分方法不實現,很明顯違背了里氏替換原則。而且,正常情況應該是畫圖工具有用鋼筆,筆刷,油漆桶畫圖的能力,而不是鋼筆,筆刷,油漆桶繼承自畫圖工具。因此,我們可以如下實現:
public class Graphics { public void Stroke(ToolEnum tool) { switch (tool) { case ToolEnum.Pen: Console.WriteLine($"用鋼筆描邊圖形"); break; case ToolEnum.Brush: Console.WriteLine($"用筆刷描邊圖形"); break; case ToolEnum.Bucket: Console.WriteLine("油漆桶不能描邊圖形"); break; default: throw new NotSupportedException("不支援的畫圖工具"); } } public void Fill(ToolEnum tool) { switch (tool) { case ToolEnum.Pen: Console.WriteLine($"鋼筆不能填充圖形"); break; case ToolEnum.Brush: Console.WriteLine($"用筆刷填充圖形"); break; case ToolEnum.Bucket: Console.WriteLine("用油漆桶填充圖形"); break; default: throw new NotSupportedException("不支援的畫圖工具"); } } }
通過上面列舉的方式,我們實現了讓畫圖工具具備描邊和填充的能力,但是這樣的switch-case
(或者if-else
)給擴充套件和維護都帶來了很大的麻煩,而且通過前面對其他設計模式的學習,我相信大家看到這樣的程式碼,一定是不能接受的。起碼應該將Pen
,Brush
,Bucket
定義成類並且繼承自同一個基類,然後組合到Graphics
中來,而不是直接使用條件判斷,因為用組合代替繼承是我們學習設計模式過程中百試不爽的經驗。但是,這次好像不怎麼靈了,因為,一旦這樣做,我們就又回到了原點---鋼筆和油漆桶不得不實現不需要的方法。事實上,用組合替代繼承是沒有錯的,但是該怎麼組合?組合誰呢?這是個問題。感覺瞬間陷入了兩難的局面,這時候策略模式就派上用場了,它不抽象鋼筆、筆刷、油漆桶等具體事物,而是直接抽象描邊和填充這兩種能力,站在程式碼的角度上看,就是將方法封裝成了物件(正應了那句一切皆物件),這正是策略模式最讓人費解,但又最妙不可言的地方。我們直接看看用策略模式改進後的程式碼是怎樣的,先抽象能力:
public interface IStrokeStrategy
{
void Stroke();
}
public class PenStrokeStrategy : IStrokeStrategy
{
public void Stroke()
{
Console.WriteLine($"用鋼筆描邊圖形");
}
}
public class BrushStrokeStrategy : IStrokeStrategy
{
public void Stroke()
{
Console.WriteLine($"用筆刷描邊圖形");
}
}
public interface IFillStrategy
{
void Fill();
}
public class BrushFillStrategy : IFillStrategy
{
public void Fill()
{
Console.WriteLine($"用筆刷填充圖形");
}
}
public class BucketFillStrategy : IFillStrategy
{
public void Fill()
{
Console.WriteLine("用油漆桶填充圖形");
}
}
看到了嗎?直接將Stroke
和Fill
兩種能力定義成了介面,再通過不同的子類去實現這種能力。然後再看看Graphics
類如何組合:
public class Graphics
{
private IStrokeStrategy _strokeStrategy;
private IFillStrategy _fillStrategy;
public Graphics(IStrokeStrategy strokeStrategy,
IFillStrategy fillStrategy)
{
this._strokeStrategy = strokeStrategy;
this._fillStrategy = fillStrategy;
}
public void Stroke()
{
this._strokeStrategy.Stroke();
}
public void Fill()
{
this._fillStrategy.Fill();
}
}
畫圖工具直接擁有了兩種能力,但是跟鋼筆、筆刷、油漆桶沒有直接關係,也就是說,只要給畫圖工具一個填充的工具,就可以完成填充功能了,至於給的具體是筆刷還是油漆桶,或者其他什麼東西,畫圖工具並不關心。而且,Graphics
類中的條件判斷語句也都去掉了,隔離了變化,整個類都變得穩定了。
如下就是通過策略模式實現的類圖:
定義
再來看一下定義,策略模式定義一系列演算法,把他們一個個封裝起來,並且使他們可以互相替換。該模式使得演算法可以獨立於使用它的客戶程式而變化。
這裡的一系列演算法就是不同的描邊和填充方式了。不同的描邊方式可以相互替換,不同的填充方式也可以相互替換,並且也可以方便的擴充套件更多的描邊和填充方式子類。
UML類圖
上面的例子中用到了兩組演算法,抽象簡化之後就得到了如下策略模式的UML類圖:
- Context:策略上下文,持有
IStrategy
的引用,負責和具體的策略實現互動; - IStrategy:策略介面,約束一系列具體的策略演算法;
- ConcreteStrategy:具體的策略實現。
優點
- 策略可以互相替換;
- 解決
switch-case
、if-else
帶來的難以維護的問題; - 策略易於擴充套件,滿足開閉原則.
缺點
- 客戶端必須知道每一個策略類,增加了使用難度。
- 隨著策略的擴充套件,策略類數量會增多;
第一個缺點無法避免,因為策略模式的一大優點就是演算法可以相互替換,但是如果使用者連每個演算法代表的是什麼意思,優缺點是什麼都不知道,又如何替換呢?但第二個缺點卻可以通過結合工廠模式,由工廠模式建立具體的策略子類來進行一定程度的緩解,至於具體該怎麼實現,這就是工廠模式的知識了,大家可以自行回憶一下。
應用場景
在業務場景中,商家促銷活動就非常適合用到策略模式了,因為,商家促銷打折可能存在會員折扣,節日折扣,生日折扣等等幾十種方式,而且在不同的條件下可以相互替換。
而非業務場景中,日誌框架中我們可能會使用log4Net
,NLog
,Serilog
等,而記錄位置也可能是控制檯,檔案,資料庫等;系統中的快取,可以用Redis
做分散式快取,也可以用MemeryCache
做本地快取等,這些場景也都非常適合使用策略模式。
總之,策略模式簡約但不簡單,學好它妙用無窮!