本文源自深入淺出設計模式. 只不過我是使用C#/.NET Core實現的例子.
前言
當你看見new這個關鍵字的時候, 就應該想到它是具體的實現.
這就是一個具體的類, 為了更靈活, 我們應該使用的是介面(interface).
有時候, 你可能會寫出這樣的程式碼:
這裡有多個具體的類被例項化了, 是根據不同情況在執行時被例項化的.
當你看到這樣的程式碼, 你就會知道當有需求需要對其進行修改或者擴充套件的時候, 你就得把這個檔案開啟, 然後看看在這裡應該新增或者刪除點什麼. 這類的程式碼經常會分散在程式的多個地方, 這維護和更新起來就很麻煩而且容易出錯.
針對interface進行程式設計的時候, 你知道可以把自己獨立於系統未來可能要發生的變化. 為什麼呢? 因為如果你針對interface程式設計, 那麼對於任何實現了該介面的具體類對你來說都可以用, 多型嗎.
專案原始需求
有一個前沿的披薩店, 做披薩, 下面是訂購披薩的類:
new一個披薩, 然後按照工序進行加工 最後返回披薩.
但是, 一個披薩店不可能只有一種披薩, 可能會有很多中披薩, 所以你可能會這樣修改程式碼:
根據傳入的型別, 建立不同的披薩, 然後加工返回.
然後問題來了, 隨著時間的推移, 一個披薩店會淘汰不暢銷的披薩並新增新品種披薩.
使用上面的程式碼就會出現這個問題, 針對需求變化, 我不得不把OrderPizza的部分程式碼改來改去:
從這裡, 我們也可以看到, 上半部分是會變化的部分, 下半部分是不變的部分, 所以它們應該分開(把變化的部分和不變的部分分開, 然後進行封裝).
結構應該是這樣的:
右上角是變化的部分, 把這部分封裝到一個物件裡, 它就是用來建立披薩的物件, 我們把這個物件叫做: 工廠.
工廠負責建立物件的細節工作. 我們建立的這個工廠叫做SimplePizzaFactory, 而orderPizza()這個方法就是該工廠的一個客戶(client).
任何時候客戶需要披薩的時候, 披薩工廠就會給客戶建立一個披薩.
接下來, 我們就建立這個簡易的披薩工廠:
就是通過傳入的型別引數, 建立並返回不同型別的披薩.
這樣我們就把披薩建立的工作封裝到了一個類裡面, 發生變化的時候, 只需要修改這一個類即可.
注意: 有時候上面這種簡單工廠可以使用靜態方法, 但是這樣也有缺點, 就是無法通過繼承來擴充套件這個工廠了.
回來修改PizzaStore這個類:
工廠是從建構函式傳入的, 並在PizzaStore裡面保留一個引用.
在OrderPizza()方法裡面, 我們使用工廠的建立方法代替了new關鍵字, 所以在這裡沒有具體的例項化.
簡單工廠的定義
簡單/簡易工廠並不是一個設計模式, 更多是一個程式設計習慣. 但是使用的非常廣泛.
簡單工廠類圖:
這個很簡單, 就不解釋了.
簡單工廠就到這, 下面要講兩個重量級的工廠模式.
用C#/.NET Core實現簡單工廠
Pizza父類:
using System; using System.Collections.Generic; namespace SimpleFactory.Pizzas { public abstract class Pizza { public string Name { get; protected set; } public string Dough { get; protected set; } public string Sauce { get; protected set; } protected List<string> Toppings = new List<string>(); public void Prepare() { Console.WriteLine($"Preparing: {Name}"); Console.WriteLine($"Tossing: {Dough}"); Console.WriteLine($"Adding sauce: {Sauce}"); Console.WriteLine("Adding toppings: "); Toppings.ForEach(x => Console.WriteLine($" {x}")); } public void Bake() { Console.WriteLine("Bake for 25 minutes"); } public void Cut() { Console.WriteLine("Cutting the pizza into diagnol slices"); } public void Box() { Console.WriteLine("Placing pizza in official PizzaStore box......"); } } }
各種Pizza:
namespace SimpleFactory.Pizzas { public class CheesePizza: Pizza { public CheesePizza() { Name = "Cheese Pizza"; Dough = "Think Dough"; Sauce = "Salad"; Toppings.Add("Grated Reggiano Cheese"); } } } namespace SimpleFactory.Pizzas { public class ClamPizza: Pizza { public ClamPizza() { Name = "Clam Pizza"; Sauce = "Tomato sauce"; Dough = "Soft dough"; Toppings.Add("Shrimp meat"); } } } namespace SimpleFactory.Pizzas { public class PepperoniPizza: Pizza { public PepperoniPizza() { Name = "Pepperoni Pizza"; Dough = "Thin dough"; Sauce = "Black pepper"; Toppings.Add("Beef Granules"); Toppings.Add("Niblet"); } } }
簡單工廠:
using SimpleFactory.Pizzas; namespace SimpleFactory { public class SimplePizzaFactory { public Pizza CreatePizza(string type) { Pizza pizza = null; switch (type) { case "cheese": pizza = new CheesePizza(); break; case "pepperoni": pizza = new PepperoniPizza(); break; case "clam": pizza = new ClamPizza(); break; } return pizza; } } }
PizzaStore:
using SimpleFactory.Pizzas; namespace SimpleFactory { public class PizzaStore { private readonly SimplePizzaFactory _factory; public PizzaStore(SimplePizzaFactory factory) { _factory = factory; } public Pizza OrderPizza(string type) { var pizza = _factory.CreatePizza(type); pizza.Prepare(); pizza.Bake(); pizza.Cut(); pizza.Box(); return pizza; } } }
測試執行:
using System; namespace SimpleFactory { class Program { static void Main(string[] args) { var pizzaStore = new PizzaStore(new SimplePizzaFactory()); var cheesePizza = pizzaStore.OrderPizza("cheese"); Console.WriteLine(); var clamPizza = pizzaStore.OrderPizza("pepperoni"); Console.ReadKey(); } } }
需求變更 - 授權連鎖披薩店
披薩店開的很好, 所以老闆在全國各地開授權連鎖分店了, 而每個地點的分店根據當地居民的口味, 它們所提供的披薩種類可能會不同.
例如紐約和芝加哥和加利福尼亞的就有可能不同.
針對這個需求, 我們可能會想到的第一種辦法就是: 把SimplePizzaFactory抽取出來, 分別建立三個地點的工廠, 然後根據地點把相應的工廠組合到PizzaStore
程式碼是這樣的:
紐約:
芝加哥:
因為個連鎖店分佈在各地, 老闆想做質量管控: 做披薩的基本工序應該是一樣的, 但是針對某種披薩各地可以有不同的風格做法.
所以我們把createPizza()方法放回到PizzaStore, 但這次它是抽象方法, 然後各地都會建立自己的PIzzaStore:
下面是紐約和芝加哥的披薩店:
針對每種披薩, 紐約和芝加哥可能會有自己風格具體實現的披薩.
orderPizza()方法是在父類/抽象類裡面實現的, 這裡的披薩還是抽象的, 所以它並不知道是PizzaStore的哪個子類來做的披薩.
程式碼執行的時候, orderPizza()會呼叫createPizza()方法, PizzaStore的某個子類肯定會對此負責.
所以你哪個地方的PizzaStore, 就會決定產出的是哪個地方特產的披薩.
下面就建立PizzaStore, 例如紐約的:
其他地點的都差不多, 就不貼圖了.
如何宣告一個工廠方法
還是看這張圖:
抽象的PizzaStore把訂購披薩的固定工序orderPizza()放在了抽象類裡面.
建立披薩createPizza()方法是在各地的披薩店裡做實現.
用一行程式碼來解釋工廠方法就是:
工廠方法是讓其子類具體來實現物件建立的工作. 這樣就把父類中的客戶程式碼和子類的建立物件部分的程式碼解耦了.
上面工作做的挺好, 但是還差一件事....披薩.
首先抽象父類:
裡面定義了調味料和工序
然後具體的披薩:
紐約的乳酪披薩
芝加哥的乳酪披薩
最後執行一下:
工廠方法模式
所有的工廠模式都會封裝物件的建立過程, 而工廠方法模式把物件建立的動作交給了子類, 並讓它決定建立哪些物件.
建立者:
產品:
看看另外一種結構 -- 並行的類結構:
工廠方法模式的定義:
工廠方法模式定義了一個建立物件的介面, 但是讓子類來決定具體建立的是哪一個物件. 工廠方法讓一個類延遲例項化, 直到子類的出現.
左邊是產品, 所有具體的產品都應該繼承於同一個父類/介面.
右邊的Creator類裡面包含所有方法的實現除了抽象的工廠方法. 這個抽象的工廠方法在Creator的子類裡面必須進行實現, 產品就是在子類具體實現的工廠方法裡面創造出來的.
設計原則 -- 應該依賴於抽象, 而不依賴於具體的類
這就是著名的: DIP (Dependency Inversion Principle) 依賴反轉原則.
進一步解釋就是: 高階別的元件不應該依賴於低階別的元件, 它們都應該依賴於抽線.
高階別元件, 就是它有一組行為定義在另外一堆低階別的元件裡面了.
例如PizzaStore就是高階別的, 具體的披薩就是低階別的.
應該該設計原則後:
這時它們都依賴於抽象的披薩父類了.
實現該原則的三點指導建議
- 沒有變數引用具體的類(可已使用工廠代替建立這個具體的類)
- 沒有類派生於具體的類(派生於它就依賴於它)
- 不去重寫(override)其任一父類的已實現方法(如果重寫了, 那麼這個類並不適合作為起始的抽象類, 因為基類裡面的方法本應該是共享與所有子類的)
和其它原則一樣, 只是盡力去按照這三點建議去執行, 並不是必須一直要這麼做.
C#/.NET Core的程式碼實現
各種pizza:
namespace FactoryMethodPattern.Pizzas { public class ChicagoCheesePizza : Pizza { public ChicagoCheesePizza() { Name = "Chicago Cheese Pizza"; Dough = "Think Dough 1"; Sauce = "Salad 1"; Toppings.Add("Grated Reggiano Cheese 1"); } } } namespace FactoryMethodPattern.Pizzas { public class ChicagoClamPizza : Pizza { public ChicagoClamPizza() { Name = "Chicago Clam Pizza"; Sauce = "Tomato sauce 1"; Dough = "Soft dough 1"; Toppings.Add("Shrimp meat 1"); } } } namespace FactoryMethodPattern.Pizzas { public class ChicagoPepperoniPizza : Pizza { public ChicagoPepperoniPizza() { Name = "Chicago Pepperoni Pizza"; Dough = "Thin dough 1"; Sauce = "Black pepper 1"; Toppings.Add("Beef Granules 1"); Toppings.Add("Niblet 1"); } } } namespace FactoryMethodPattern.Pizzas { public class NYCheesePizza: Pizza { public NYCheesePizza() { Name = "NY Cheese Pizza"; Dough = "Think Dough 2"; Sauce = "Salad 2"; Toppings.Add("Grated Reggiano Cheese 2"); } } } namespace FactoryMethodPattern.Pizzas { public class NYClamPizza: Pizza { public NYClamPizza() { Name = "NY Clam Pizza"; Sauce = "Tomato sauce 2"; Dough = "Soft dough 2"; Toppings.Add("Shrimp meat 2"); } } } namespace FactoryMethodPattern.Pizzas { public class NYPepperoniPizza: Pizza { public NYPepperoniPizza() { Name = "NY Pepperoni Pizza"; Dough = "Thin dough 2"; Sauce = "Black pepper 2"; Toppings.Add("Beef Granules 2"); Toppings.Add("Niblet 2"); } } }
披薩店抽象父類:
using FactoryMethodPattern.Pizzas; namespace FactoryMethodPattern { public abstract class PizzaStore { public Pizza OrderPizza(string type) { var pizza = CreatePizza(type); pizza.Prepare(); pizza.Bake(); pizza.Cut(); pizza.Box(); return pizza; } protected abstract Pizza CreatePizza(string type); } }
Chicago披薩店:
using FactoryMethodPattern.Pizzas; namespace FactoryMethodPattern { public class ChicagoPizzaStore: PizzaStore { protected override Pizza CreatePizza(string type) { Pizza pizza = null; switch (type) { case "cheese": pizza = new ChicagoCheesePizza(); break; case "pepperoni": pizza = new ChicagoPepperoniPizza(); break; case "clam": pizza = new ChicagoClamPizza(); break; } return pizza; } } }
紐約披薩店:
using FactoryMethodPattern.Pizzas; namespace FactoryMethodPattern { public class NYPizzaStore : PizzaStore { protected override Pizza CreatePizza(string type) { Pizza pizza = null; switch (type) { case "cheese": pizza = new NYCheesePizza(); break; case "pepperoni": pizza = new NYPepperoniPizza(); break; case "clam": pizza = new NYClamPizza(); break; } return pizza; } } }
測試執行:
using System; namespace FactoryMethodPattern { class Program { static void Main(string[] args) { var nyStore = new NYPizzaStore(); var chicagoStore = new ChicagoPizzaStore(); var pizza = nyStore.OrderPizza("cheese"); Console.WriteLine($"Ordered a {pizza.Name} in NY"); Console.WriteLine(); var pizza2 = chicagoStore.OrderPizza("cheese"); Console.WriteLine($"Ordered a {pizza2.Name} in Chicago"); Console.ReadKey(); } } }