笑說設計模式-小白逃課被點名

簡介

工廠模式(Factory Pattern)是最常用的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。

在工廠模式中,我們在創建對象時不會對客戶端暴露創建邏輯,而是通過使用一個共同的介面來指向新創建的對象。

分類

工廠模式可以分為三種,其中簡單工廠一般不被認為是一種設計模式,可以將其看成是工廠方法的一種特殊。

  • 簡單工廠
  • 工廠方法
  • 抽象工廠

場景分析

平凡枯燥的文字總是讓人看得想睡覺,接下來我們用幾個情景案例來進行分析

簡單工廠

直接通過一個Factory【工廠類】類創建多個實體類的構造方式。

時間:2021年2月19日 地點:教室 人物:學生小白、老師、大佬黑皮


小白是一名大二的電腦系學生,懶惰不愛學習。今天早晨第一節課就因為睡懶覺遲到被老師逮個正著,這節課還正好是小白最頭疼的上機課”C#設計模式」。這不,課堂上到一半老師就開始提問,小白「光榮」的成為了老師的點名對象。

老師笑著說道:「小白,請你解答一下螢幕上的問題。」

題目:請使用c#、java、python、php或其他任一面向對象程式語言實現輸入倆個合法數字和一個合法符號,輸出對應結果的功能。

小白一看,這算什麼題目,這麼簡單,看我不手到擒來,伴隨著雙手噼里啪啦一頓敲擊聲音,螢幕上出現一串編碼。

**Calculator操作類 **

    public class Calculator
    {
        public double GetResult(double A, double B, string operate)
        {
            double result = 0d;
            switch (operate)
            {
                case "+": 
                    result = A + B;
                    break;
                case "-":
                    result = A - B;
                    break;
                case "*":
                    result = A * B;
                    break;
                case "/":
                    result = A / B;
                    break;
                default: break;
            }
            return result;
        }
    }

客戶端程式碼

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("請輸入數字A");
            string a = Console.ReadLine();
            Console.WriteLine("請輸入數字B");
            string b = Console.ReadLine();
            Console.WriteLine("請選擇操作符號(+、-、*、/)");
            string operate = Console.ReadLine();

            Calculator box = new Calculator();
            double result = box.GetResult(Convert.ToDouble(a), Convert.ToDouble(b), operate);
            Console.WriteLine(result);
            
            Console.ReadKey();
        }
    }

小白:」老師,我寫好了「

老師:”不錯,寫的很好。使用到了面向對象三大特性中的封裝,將計算方法封裝成了一個計算類,多個客戶端可以復用這個類。但是這其中也有不少的問題,哪位同學來回答一下。”

黑皮:」小白同學寫的程式碼有倆處問題。

1、如果輸入A=10,B=0,程式就會報錯,沒有做輸入的有效性驗證

2、如果操作符號不按照規定的輸入 ,也會導致報錯「

老師:」黑皮同學回答的非常好,但是這都只是針對程式碼業務邏輯錯誤的描述。有沒有誰可以看出更加深層次的內容。「

黑皮:」老師,你給點提示吧。「

老師:」這個可以會有點隱蔽,老師就給出一點提示。如果我們增加一個新的運算怎麼辦?「

小白立即回答到:」在switch中增加一個新的分支就可以了「。

老師:」這樣當然是沒有錯誤的,但是問題在於,我只是增加了一個新的運算符號,卻需要讓加減乘除所有的運算都參加編譯,如果在修改的過程中不小心修改了其他的程式碼,例如把+號改成了-號,那不是很糟糕,這就違背了開閉原則【對擴展開放,對修改關閉】「

小白撓一撓頭問道:」開閉原則,這是什麼?「

老師:」這就是你不認真聽課落下的內容,回去好好補習。黑皮同學,不知道你Get到了老師的點沒有?「

黑皮:」我知道應該如何修改了。小白實現了面向對象三大特性之一的封裝,其實就是將其他倆個特性一起使用就可以完成老師要的功能「

小白:」多態和繼承?「

黑皮:」是的,等我改完你再看程式就應該有感覺了「

    public class Operate
    {
        public double NumberA { get; set; }
        public double NumberB { get; set; }

        public virtual double GetResult()
        {
            return 0;
        }
    }
    
    public class OperateAdd : Operate
    {
        public override double GetResult()
        {
            return this.NumberA +this.NumberB;
        }
    }
    
    public class OperateSub : Operate
    {
        public override double GetResult()
        {
            return this.NumberA - this.NumberB;
        }
    }

簡單工廠

    public class OperateFactory
    {
        public static Operate GetOperateFactory(string operate)
        {
            Operate fac = null;
            switch (operate)
            {
                case "+":
                    fac = new OperateAdd();
                    break;
                case "-":
                    fac = new OperateSub();
                    break;
                case "*":
                    fac = new OperateMul();
                    break;
                case "/":
                    fac = new OperateDiv();
                    break;
                default:
                    break;
            }
            return fac;
        }
    }

黑皮:」首先是一個運算類,裡面有倆個Number屬性和一個虛方法GetResult()。加減乘除四個方法作為運算類的子類繼承,繼承後重寫GetResult()方法,調用基類的A和B公有屬性進行不同的數學運算。「

黑皮:」然後定義一個簡單工廠,靜態方法傳入操作符參數得到實際的業務處理類,客戶端得到處理類後對參數賦值。最後一步你應該知道怎麼做了吧「

小白:」我懂了,那客戶端這麼調用就可以了「。

  static void Main(string[] args)
        {
            Console.WriteLine("請選擇操作符號(+、-、*、/)");
            string operateStr = Console.ReadLine();
            Operate operate = OperateFactory.GetOperateFactory(operateStr);
            operate.NumberA = 10;
            operate.NumberB = 4;
            double result = operate.GetResult();
            Console.WriteLine(result);
            Console.ReadKey();
        }

老師:」看來倆位同學都已經掌握了簡單工廠的使用,接下來我提問幾個問題,便於大家更快的掌握這種設計模式「

老師:」如果我們要修改除方法的邏輯,增加被除數為0的邏輯應該怎麼做「

小白:」直接修改OperateDiv類,這不會對其他造成影響「

老師:」如果我們要新增一個開根運算應該怎麼做「

小白:」添加一個新的類,Operate開根類,裡面描述開根的邏輯。在工廠方法中將新的操作符添加進去即可。新增的操作單獨一個類,也不會對其他方法體造成影響「。

小白:」那客戶端需要做什麼改變嗎?「

老師:」客戶端要做什麼改變,客戶端只要處理好自己的事情就可以了!「

是的,客戶端不關心工廠創建了什麼,工廠是一個黑盒子。客戶端只要傳入參數,工廠負責將內容生產後【實例化類的過程】給客戶端即可。

優/缺點

簡單工廠模式的工廠類一般是使用靜態方法,通過接收的參數不同來返回不同的對象實例。不修改程式碼的話,是無法擴展的
優點:客戶端可以免除直接創建產品對象的責任,而僅僅是「消費」產品。簡單工廠模式通過這種做法實現了對責任的分割
缺點:由於工廠類集中了所有實例的創建邏輯,違反了高內聚責任分配原則,將全部創建邏輯集中到了一個工廠類中;它所能創建的類只能是事先考慮到的,如果需要添加新的類,則就需要改變工廠類了

工廠方法

時間:2021年2月19日下午 地點:教室 人物:學生小白、老師、大佬黑皮

老師:」我們緊接著上午的設計模式繼續,上午我們講的是簡單工廠,下午我們講下一個內容工廠方法。工廠方法和簡單工廠其實大同小異,唯一的區別就在於每一個實現抽象類的實例(也叫做產品,即上午定義的加減乘除四個子類)都有一個對應的工廠去創建。同學們了解一下老師說話的內容,然後尋找一個場景編碼實現一下。最快完成的有課堂獎勵」

….幾分鐘過去了…..

小白:「老師,我完成了。」

老師:「好的,那我們請小白同學說明一下場景和實現的方式。」

我設計的是以水果作為場景的模式。

1、定義一個抽象類Fruit.cs,這個類定義倆個抽象方法printfColor()printfName()

2、實現倆種不同的水果分別繼承此抽象類並複寫抽象方法。

3、定義一個工廠介面,定義介面方法createFruit()

4、實現倆個不同的工廠分別實現水果實例的創建。

水果抽象類

    public abstract class Fruit
    {
        public abstract void PrintfColor();
        public abstract void PrintfName();
    }
    
  public class Apple : Fruit
    {
        public override void PrintfColor()
        {
            Console.WriteLine("紅色");
        }

        public override void PrintfName()
        {
            Console.WriteLine("蘋果");
        }
    }

工廠介面

   public interface IFruitFactory
    {
        Fruit CreateFruit();
    }

    public class AppleFactory : IFruitFactory
    {
        public Fruit CreateFruit()
        {
            return new Apple();
        }
    }

客戶端實現

           //蘋果工廠
            IFruitFactory appleFac = new AppleFactory();
            Fruit apple = appleFac.CreateFruit();
            apple.PrintfColor();
            apple.PrintfName
            
            //橘子工廠
            IFruitFactory orangeFac = new OrangeFactory();
            Fruit orage = orangeFac.CreateFruit();
            orage.PrintfColor();
            orage.PrintfName();

老師:「看來小白同學已經對上午的內容有了一個充分的了解,果然好好上課才能夠學習到新的知識。逃課是沒有益處的」

老師:「只是這樣的案例太過簡單,可能其他同學不是很能理解為什麼要這樣 ,我來舉一個實際場景的案例方便大家理解。在實際的工作過程中我們總會用到日誌組件,例如Nlog,Log4net這種第三方組件,這種組件都支援可配置化的多源輸出。當我們在配置文件(json/xml)中增加一個「輸出到控制台的參數」,程式 就會將內容輸出到控制台,當配置一個輸入到文件的參數,程式就會將內容輸出到指定的文件。這種場景的實現其實就是一種典型的工廠方法。下面我來分析一下過程」

1、讀取配置文件(json/xml)

2、獲取所有的配置方式,循環遍歷

3、判斷配置類型,如果是輸入到文件的配置。new一個文件日誌工廠,將配置資訊作為參數傳遞,便於後期方法調用;如果是輸入到控制台的配置。new一個日誌工廠也是做同樣的操作

4、每一個工廠只管理自己的事情,但是應該都擁有日誌輸出這個介面。

5、當上層調用列印方法時候,循環遍歷所有的工廠,調用介面的日誌輸出輸出方法

優/缺點

工廠方法是針對每一種產品提供一個工廠類。通過不同的工廠實例來創建不同的產品實例。在同一等級結構中,支援增加任意產品
優點:允許系統在不修改具體工廠角色的情況下引進新產品
缺點:由於每加一個產品,就需要加一個產品工廠的類,增加了額外的開發量

抽象工廠

抽象工廠模式為創建一組對象提供了一種解決方案。與工廠方法模式相比,抽象工廠模式中的具體工廠不只是創建一種產品,它負責創建一族產品。

時間:2021年2月20日上午 地點:教室 人物:學生小白、老師、黑皮

新的一天又開始了,「設計模式」課程在小白的眼中好像沒有那麼複雜了,今天小白早早地就到了教室,準備迎接老師新的鞭策。

老師:」同學們早上好,今天我們繼續昨日的課程。昨天講的是工廠方法,今天我們在此基礎上做一點改進,看看又有什麼新的變化。小白同學學習熱情很高嘛,現在都知道坐在第一排了。不錯不錯,值得鼓勵」

小白:」嘻嘻「

老師:「好,那開始今天的課程。今天要講的模式是抽象工廠模式。通過和工廠模式做比較,同學們可以比較清晰的發現這倆都之間的區別。我們用昨天小白同學的例子繼續開拓。」

此時有蘋果和橘子倆個產品,分別對應蘋果工廠和橘子工廠。這是工廠方法的體現。可是如果有3個不同的工廠,他們分別都生產蘋果和橘子呢。

小白:「恩…那就多建立幾個工廠。每個產品分別對應不同的工廠,應該是這樣的一個結構,每一個工廠分別對應生產產品的類」

A

  • A_蘋果工廠.cs
  • A_橘子工廠.cs

B

  • B_蘋果工廠.cs
  • B_橘子工廠.cs

C

  • C_蘋果工廠.cs
  • C_橘子工廠.cs

老師:「這樣的方式當然是可以的,可以如果我有10個工廠呢,難道我們要建立10*2=20個類嗎,這樣程式的複雜度就是直線上升,不利於維護。」

小白:「那怎麼辦呢,用老師你說的那種抽象工廠嗎?如果用,又應該怎麼做呢」

老師:「是的,在這樣的場景下,抽象工廠是最能匹配的設計模式。其實做法非常簡單,對昨天的程式碼進行一些修改即可」

水果抽象類

新增一個Name屬性,方便後期列印不同的工廠。

    public abstract class Fruit
    {
        public string Name { get; set; }
        public abstract void PrintfColor();
        public abstract void PrintfName();
    }
    
    public class Apple : Fruit
    {
        public Apple(string name)
        {
            this.Name = name;
        }

        public override void PrintfColor()
        {
            Console.WriteLine(this.Name + "紅色");
        }

        public override void PrintfName()
        {
            Console.WriteLine(this.Name + "蘋果");
        }
    }

工廠介面

老師:「這一處的改動就比較明顯。原來的介面中方法輸出唯一的產品——因為之前每一個工廠只生產一件產品。現在輸出倆個產品,即繼承工廠介面的類必須實現生產蘋果和橘子的方法。這樣的好處在於,每一個工廠負責管理自己產品的實現,避免了每一個產品都需要創建一個工廠的操作。「

解釋:

工廠生產蘋果和橘子。當有多個工廠的時候,每一個工廠都實現生產蘋果和橘子。而不是生產A廠蘋果需要一個工廠實現類,生產B廠蘋果又需要一個。如下所示

舊模式

A

  • A_蘋果工廠.
  • A_橘子工廠

B

  • B_蘋果工廠
  • B_橘子工廠

C

  • C_蘋果工廠

新模式

A 工廠

  • 蘋果/橘子

B 工廠

  • 蘋果/橘子

C 工廠

  • 蘋果/橘子

老師:「這樣複雜度由原來的6變成了3。」

小白:”我明白了,又學習到了新的東西。”

    public interface IFruitFactory
    {
        Fruit CreateApple(string name);
        Fruit CreateOrange(string name);
    }

    public class AFactory : IFruitFactory
    {
        public Fruit CreateApple(string name)
        {
            return new Apple(name);
        }

        public Fruit CreateOrange(string name)
        {
            return new Orange(name);
        }
    }

客戶端實現

            IFruitFactory fac = new AFactory();
            Fruit a_Apple = fac.CreateApple("a工廠");
            Fruit a_Orange = fac.CreateOrange("a工廠");
            a_Apple.PrintfName();
            a_Orange.PrintfName();

            IFruitFactory b_fac = new BFactory();
            Fruit b_Apple = b_fac.CreateApple("b工廠");
            Fruit b_Orange = b_fac.CreateOrange("b工廠");
            b_Apple.PrintfName();
            b_Orange.PrintfName();

小白:「可是在什麼樣的場景下用這種模式呢,我好像一下子想不到」

老師:「抽象工廠的使用相對來說比較少,但也不是沒有。我舉一個例子,在後端開始中我們有各種的組件【按鈕,抽屜,導航欄】等等,這些組件有對應的皮膚,對皮膚的開發就是抽象工廠的實現。工廠介面是對每個組件的定義,每個皮膚就是一個工廠的實現。如果要切換皮膚,只需要實例化不同的工廠即可。」

小白:「哦。就和遊戲中的皮膚切換一樣嗎?」

老師:「你也可以這樣理解,設計模式只是一種通用解決方案,可以應用在不同的場景下,大家可以挑最適應自己,最好理解的場景下手。」

下課鈴聲又響起了

老師:「好了,這節課就到這裡。下節課我們講其他的設計模式,希望大家準時聽講。」

優/缺點

抽象工廠是應對產品族概念的。應對產品族概念而生,增加新的產品線很容易,但是無法增加新的產品。比如,每個汽車公司可能要同時生產轎車、貨車、客車,那麼每一個工廠都要有創建轎車、貨車和客車的方法
優點:向客戶端提供一個介面,使得客戶端在不必指定產品具體類型的情況下,創建多個產品族中的產品對象
缺點:增加新的產品等級結構很複雜,需要修改抽象工廠和所有的具體工廠類,對「開閉原則」的支援呈現傾斜性