1. 程式人生 > 實用技巧 >【Python學習日記】B站小甲魚:永久儲存(pickle模組)和異常處理(exception)

【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)給擴充套件和維護都帶來了很大的麻煩,而且通過前面對其他設計模式的學習,我相信大家看到這樣的程式碼,一定是不能接受的。起碼應該將PenBrushBucket定義成類並且繼承自同一個基類,然後組合到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("用油漆桶填充圖形");
    }
}

看到了嗎?直接將StrokeFill兩種能力定義成了介面,再通過不同的子類去實現這種能力。然後再看看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-caseif-else帶來的難以維護的問題;
  • 策略易於擴充套件,滿足開閉原則.

缺點

  • 客戶端必須知道每一個策略類,增加了使用難度。
  • 隨著策略的擴充套件,策略類數量會增多;

第一個缺點無法避免,因為策略模式的一大優點就是演算法可以相互替換,但是如果使用者連每個演算法代表的是什麼意思,優缺點是什麼都不知道,又如何替換呢?但第二個缺點卻可以通過結合工廠模式,由工廠模式建立具體的策略子類來進行一定程度的緩解,至於具體該怎麼實現,這就是工廠模式的知識了,大家可以自行回憶一下。

應用場景

在業務場景中,商家促銷活動就非常適合用到策略模式了,因為,商家促銷打折可能存在會員折扣,節日折扣,生日折扣等等幾十種方式,而且在不同的條件下可以相互替換。
而非業務場景中,日誌框架中我們可能會使用log4Net,NLog,Serilog等,而記錄位置也可能是控制檯,檔案,資料庫等;系統中的快取,可以用Redis做分散式快取,也可以用MemeryCache做本地快取等,這些場景也都非常適合使用策略模式。

總之,策略模式簡約但不簡單,學好它妙用無窮!

原始碼連結