開篇 天天逛博客園,就是狠不下心來寫篇博客,忙是一方面,但是說忙能有多忙呢,都有時間逛博客園,寫篇博客的時間都沒有?(這還真不好說) 每次想到寫一篇新的設計模式,我總會問自己: 1,自己理解了嗎? 2,我能以一種簡單且有邏輯的方式把它說出來嗎? 不說做到有的放矢,但是一本正經的胡說八道還是要有吧,起 ...
開篇
天天逛博客園,就是狠不下心來寫篇博客,忙是一方面,但是說忙能有多忙呢,都有時間逛博客園,寫篇博客的時間都沒有?(這還真不好說)
每次想到寫一篇新的設計模式,我總會問自己:
1,自己理解了嗎?
2,我能以一種簡單且有邏輯的方式把它說出來嗎?
不說做到有的放矢,但是一本正經的胡說八道還是要有吧,起碼要忽悠得頭頭是道嘛(手動斜眼笑)
關於工廠模式的幾個問題
1,這個是拿來乾什麼的?
2,怎麼用?
3,不用行不行?
第一個和第三個問題,我現在就可以告訴你答案:早點下班,可以
所有的設計模式對我來說都是為了減少工作量。關於減少工作量我的理解是:每個需求,都應該在它適當的時候出現適當的代碼!這個太重要了
代碼偷懶,後期返工多
過度設計,後期返工多
設計模式+經驗可以解決這個問題,其他的我還不知道。沒有經驗怎麼辦?兩個要點:
1,能用
2,簡潔
首先要達到能用,然後就是儘量簡潔,這樣代碼就不會太差。首先你要自己看得懂,然後是讓隊友看得懂。
你知道你隊友看到一堆爛的看都看不懂,也一句註釋都沒有的代碼的時候的心理陰影面積嗎?
這其實也沒什麼,誰沒填過別人的坑呢?關鍵是他知道你家在哪裡,而且還知道你經常走夜路,就問你怕不怕?(卧槽,又跑題了。。)
需求:你有一個披薩店,只賣一種披薩,代碼如下:
披薩:
import java.util.ArrayList; import java.util.List; /** * 披薩類 * @author skysea */ public class Pizza { private String name;//披薩名稱 private String dough;//麵團 private String sauce;//醬料 private List<String> toppings = new ArrayList<>();//佐料 public Pizza() { this.name = "原味披薩"; this.dough = "原味麵團"; this.sauce = "原味醬料"; } void prepare() { System.out.println("開始準備披薩:" + name); System.out.println("開始處理麵團:" + dough); System.out.println("添加醬料:" + sauce); System.out.println("添加佐料:"); if(toppings.size() > 0) { for(String t : toppings) { System.out.println(" " + t); } } } void bake() { System.out.println("烘焙25分鐘.."); } void cut() { System.out.println("披薩切片.."); } void box() { System.out.println("披薩打包.."); } public String getName() { return name; } }
披薩店:
/** * 只賣一種披薩的披薩店 * @author skysea */ public class PizzaStore { public Pizza orderPizza() { Pizza pizza = new Pizza(); pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza; } }
測試類:
/** * pizza測試類 * @author skysea */ public class PizzaTest { public static void main(String[] args) { PizzaStore pizzaStore = new PizzaStore(); Pizza pizza = pizzaStore.orderPizza(); System.out.println("當前預定的披薩:" + pizza.getName()); } }
現在披薩店要拓展業務了,因為賣一種披薩顧客已經吃膩了,現在要開始添加新的披薩類型
簡單工廠模式
Pizza類的改進
import java.util.ArrayList; import java.util.List; /** * 披薩抽象類 * 1,修改private -> protected(保證子類擁有這些屬性) * 2,將Pizza定義為abstract類,防止被new,也是為後面的改造做準備 * @author skysea */ public abstract class Pizza { protected String name;//披薩名稱 protected String dough;//麵團 protected String sauce;//醬料 protected List<String> toppings = new ArrayList<>();//佐料 void prepare() { System.out.println("開始準備披薩:" + name); System.out.print("開始處理麵團:" + dough); System.out.println("添加醬料:" + sauce); System.out.println("添加佐料:"); for(String t : toppings) { System.out.println(" " + t); } } void bake() { System.out.println("烘焙25分鐘.."); } void cut() { System.out.println("披薩切片.."); } void box() { System.out.println("披薩打包.."); } public String getName() { return name; } @Override public String toString() { return "Pizza [name=" + name + ", dough=" + dough + ", sauce=" + sauce + ", toppings=" + toppings + "]"; } }
先給出新增的披薩
芝士披薩:
/** * 芝士披薩 * @author skysea */ public class CheesePizza extends Pizza{ public CheesePizza() { this.name = "芝士披薩"; this.dough = "芝士披薩的麵團"; this.sauce = "芝士披薩的醬料"; this.toppings.add("很多芝士...."); } }View Code
蛤蜊披薩:
/** * 蛤蜊披薩 * @author skysea */ public class ClamPizza extends Pizza { public ClamPizza() { this.name = "蛤蜊披薩"; this.dough = "蛤蜊披薩的麵團"; this.sauce = "蛤蜊披薩的醬料"; this.toppings.add("蛤蜊"); } }View Code
義大利烤腸披薩:
/** * 義大利烤腸披薩 * @author skysea */ public class PepperoniPizza extends Pizza{ public PepperoniPizza() { this.name = "義大利烤腸披薩"; this.dough = "義大利烤腸披薩的麵團"; this.sauce = "義大利烤腸披薩的醬料"; this.toppings.add("一大波義大利烤腸..."); } }View Code
素食比薩:
/** * 素食比薩 * @author skysea */ public class VeggiePizza extends Pizza { public VeggiePizza() { name = "素食比薩"; dough = "素食比薩的麵團"; sauce = "素食比薩的醬料"; toppings.add("素食比薩"); toppings.add("素食比薩佐料1"); toppings.add("素食比薩佐料2"); } }View Code
貼了這麼多代碼,先給出一波簡單的實現:
/** * pizza店 * @author skysea */ public class PizzaStore { public Pizza orderPizza(String type) { Pizza pizza = null; if (type.equals("cheese")) { pizza = new CheesePizza(); } else if (type.equals("pepperoni")) { pizza = new PepperoniPizza(); } else if (type.equals("clam")) { pizza = new ClamPizza(); } else if (type.equals("veggie")) { pizza = new VeggiePizza(); } pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza; } }
在不考慮繼續擴展披薩種類的時候,這樣的實現有沒有問題,一般來說,達到了可以用的標準,但是不好用,問題如下:
1,沒有相容原來的一種披薩方法 public Pizza orderPizza(),相信我,每一個public方法都是很重要的,因為你不知道有多少地方用到過。當然也不是沒辦法知道,只是你知道也不一定就能改,就算你能改,也不一定改對。
2,String類型的type太容易出錯了,個人感覺對程式開發不友好,當然這個也要分情況,靈活和嚴謹本來就很難做到兩全
3,推薦取不到合適的type時拋異常,而不是返回空,便於排查問題(此處的if裡面只是直接new返回的對象,實際情況遠比現在的複雜)
給出第二版:
/** * pizza店 * @author skysea */ public class PizzaStore { public Pizza orderPizza() { return orderPizza(PizzaTypeEnum.CHEESE); } public Pizza orderPizza(PizzaTypeEnum type) { Pizza pizza; pizza = SimplePizzaFactory.getPizza(type); pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza; } }
SimplePizzaFactory:
/** * 簡單工廠類 * @author skysea */ public class SimplePizzaFactory { /** * 根據類型獲取pizza * @param type * @return */ public static final Pizza getPizza(PizzaTypeEnum type){ switch (type) { case CHEESE: return new CheesePizza(); case CLAM: return new ClamPizza(); case PEPPERONI: return new PepperoniPizza(); case VEGGIE: return new VeggiePizza(); default: throw new NoSuchPizzaException(type.getCode()); } } }
輔助類(枚舉,異常):
/** * 定義pizza類型枚舉 * @author skysea * */ public enum PizzaTypeEnum{ /** * 芝士披薩 */ CHEESE("cheese"), /** * 義大利烤腸披薩 */ PEPPERONI("pepperoni"), /** * 蛤蜊披薩 */ CLAM("clam"), /** * 素食比薩 */ VEGGIE("veggie"); private final String code; PizzaTypeEnum(String code) { this.code = code; } public String getCode() { return code; } }View Code
/** * 沒有匹配的pizza異常 * @author skysea */ public class NoSuchPizzaException extends RuntimeException{ private static final long serialVersionUID = 6831396172306375611L; public NoSuchPizzaException(String message) { super(message); } }View Code
測試類:
/** * pizza測試類 * @author skysea */ public class PizzaTest { public static void main(String[] args) { PizzaStore store = new PizzaStore(); Pizza pizza = store.orderPizza(PizzaTypeEnum.CHEESE); System.out.println(pizza); pizza = store.orderPizza(PizzaTypeEnum.VEGGIE); System.out.println(pizza); } }
好了,代碼寫到這裡,其實對於:新增披薩類型的這個需求的實現其實已經很好了。至少來說現階段的需求實現了,其次就是對調用方友好,至少隊友不會跑過來問你類型傳啥,不會告訴你他string字元串傳錯了,不會在你改個類型的時候,還要通知他(當然這個也可以通過常量來處理)。
吹了半天,來說說這段代碼的問題,正常情況下,需求會是這樣變:
1,PepperoniPizza暫時不要了,一般來說,你問他要不要,他會說,這個要看後面的運營情況(我:...)
2,你給我新加一個xx披薩
現在需要改的是兩個地方,一個是工廠類,一個是枚舉,但是主要的流程是不用改了,如果你覺得還是很麻煩在不考慮性能的情況下,你還可以用反射來玩,改造一下工廠類(實現通過class來創建對象)和枚舉(添加一個欄位來存放type對應的class)就可以了,不贅述..
第一波需求就差不多可以這樣收手了,隨著業務的發展,披薩店那叫一個紅火啊,雖然中間也對代碼做了很多新的披薩,但是由於PizzaStore相當穩定,也沒出什麼大問題。
新的問題(開分店):
1,旗艦店在芝加哥,現在要在紐約開一家新的店
2,分店的披薩口味要根據當地的口味來進行調整,保證能夠不失品牌特色的同時,也能滿足當地獨特的風味
3,分店披薩的種類與暫時與旗艦店保持一致
工廠方法模式
先把所有的披薩列出來
芝加哥的披薩:
/** * 芝加哥芝士披薩 * @author skysea */ public class ChicagoStyleCheesePizza extends Pizza { public ChicagoStyleCheesePizza() { name = "芝加哥芝士披薩"; dough = "芝加哥芝士披薩麵團"; sauce = "芝加哥芝士披薩醬料"; toppings.add("芝加哥芝士披薩調料1"); toppings.add("芝加哥芝士披薩調料2"); } @Override void cut() { System.out.println("芝加哥芝士披薩版切片..."); } } /** * 芝加哥蛤蜊披薩 * @author skysea */ public class ChicagoStyleClamPizza extends Pizza { public ChicagoStyleClamPizza() { name = "芝加哥蛤蜊披薩"; dough = "芝加哥蛤蜊披薩麵團"; sauce = "芝加哥蛤蜊披薩醬料"; toppings.add("芝加哥蛤蜊披薩佐料1"); toppings.add("芝加哥蛤蜊披薩佐料2"); } @Override void cut() { System.out.println("芝加哥蛤蜊披薩版切片..."); } } /** * 芝加哥義大利烤腸披薩 * @author skysea */ public class ChicagoStylePepperoniPizza extends Pizza { public ChicagoStylePepperoniPizza() { name = "芝加哥義大利烤腸披薩"; dough = "芝加哥義大利烤腸披薩麵團"; sauce = "芝加哥義大利烤腸披薩醬料"; toppings.add("芝加哥義大利烤腸披薩調料1"); toppings.add("芝加哥義大利烤腸披薩調料2"); toppings.add("芝加哥義大利烤腸披薩調料3"); toppings.add("芝加哥義大利烤腸披薩調料4"); } @Override void cut() { System.out.println("芝加哥義大利烤腸披薩版切片..."); } } /** * 芝加哥素食比薩 * @author skysea */ public class ChicagoStyleVeggiePizza extends Pizza { public ChicagoStyleVeggiePizza() { name = "芝加哥素食比薩"; dough = "芝加哥素食比薩的麵團"; sauce = "芝加哥素食比薩的醬料"; toppings.add("芝加哥素食比薩調料1"); toppings.add("芝加哥素食比薩調料2"); toppings.add("芝加哥素食比薩調料3"); } void cut() { System.out.println("芝加哥素食比薩版切片..."); } }View Code
紐約的披薩:
/** * 紐約芝士披薩 * @author skysea */ public class NYStyleCheesePizza extends Pizza { public NYStyleCheesePizza() { name = "紐約芝士披薩"; dough = "紐約芝士披薩麵團"; sauce = "紐約芝士披薩醬料"; toppings.add("紐約芝士披薩調料1"); toppings.add("紐約芝士披薩調料2"); } @Override void cut() { System.out.println("紐約芝士披薩版切片..."); } } /** * 紐約蛤蜊披薩 * @author skysea */ public class NYStyleClamPizza extends Pizza { public NYStyleClamPizza() { name = "紐約蛤蜊披薩"; dough = "紐約蛤蜊披薩麵團"; sauce = "紐約蛤蜊披薩醬料"; toppings.add("紐約蛤蜊披薩佐料1"); toppings.add("紐約蛤蜊披薩佐料2"); } @Override void cut() { System.out.println("紐約蛤蜊披薩版切片..."); } } /** * 紐約義大利烤腸披薩 * @author skysea */ public class NYStylePepperoniPizza extends Pizza { public NYStylePepperoniPizza() { name = "紐約義大利烤腸披薩"; dough = "紐約義大利烤腸披薩麵團"; sauce = "紐約義大利烤腸披薩醬料"; toppings.add("紐約義大利烤腸披薩調料1"); toppings.add("紐約義大利烤腸披薩調料2"); toppings.add("紐約義大利烤腸披薩調料3"); toppings.add("紐約義大利烤腸披薩調料4"); } @Override void cut() { System.out.println("紐約義大利烤腸披薩版切片..."); } } /** * 紐約素食比薩 * @author skysea */ public class NYStyleVeggiePizza extends Pizza { public NYStyleVeggiePizza() { name = "紐約素食比薩"; dough = "紐約素食比薩的麵團"; sauce = "紐約素食比薩的醬料"; toppings.add("紐約素食比薩調料1"); toppings.add("紐約素食比薩調料2"); toppings.add("紐約素食比薩調料3"); } void cut() { System.out.println("紐約素食比薩版切片..."); } }View Code
披薩倒是列完了,但是在實際的開發過程中,業務邏輯這麼簡單那是不可能的,想要改那什麼旗艦店披薩的類名是很困難的
一般要考慮:
1,是不是單機,有沒有其他外部系統在調用
2,改動原來的代碼有什麼好處,更容易理解嗎?迭代了幾個版本過後垃圾代碼太多了嗎?
3,影響大不大
當然,我這裡是隨便造,你們呢,我就不知道了,嘿嘿嘿,所以碰到這種情況,一般來說要悠著點,看時間,也要看影響,開發就是這樣,同一個功能,2天有2天的做法,5天有5天的做法,10天有10天的做法
披薩店改造:
/** * 披薩店抽象類 * @author skysea */ public abstract class PizzaStore { abstract Pizza createPizza(String item); public Pizza orderPizza(String type) { Pizza pizza = createPizza(type); System.out.println("--- 製作 " + pizza.getName() + " ---"); pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza; } }
/** * 芝加哥披薩店 * @author skysea */ public class ChicagoPizzaStore extends PizzaStore { public static final String CHEESE = "cheese"; public static final String VEGGIE = "veggie"; public static final String CLAM = "clam"; public static final String PEPPERONI = "pepperoni"; Pizza createPizza(String item) { if (CHEESE.equals(item)) { return new ChicagoStyleCheesePizza(); } else if (VEGGIE.equals(item)) { return new ChicagoStyleVeggiePizza(); } else if (CLAM.equals(item)) { return new ChicagoStyleClamPizza(); } else if (PEPPERONI.equals(item)) { return new ChicagoStylePepperoniPizza(); } else { throw new NoSuchPizzaException(item); } } }
紐約披薩店(和芝加哥披薩店幾乎一毛一樣,這裡就不展開了):
/** * 紐約披薩店 * @author skysea */ public class NYPizzaStore extends PizzaStore { public static final String CHEESE = "cheese"; public static final String VEGGIE = "veggie"; public static final String CLAM = "clam"; public static final String PEPPERONI = "pepperoni"; Pizza createPizza(String item) { if (CHEESE.equals(item)) { return new NYStyleCheesePizza(); } else if (VEGGIE.equals(item)) { return new NYStyleVeggiePizza(); } else if (CLAM.equals(item)) { return new NYStyleClamPizza(); } else if (PEPPERONI.equals(item)) { return new NYStylePepperoniPizza(); } else { throw new NoSuchPizzaException(item); } } }View Code
這段代碼有三個問題要理解清楚:
1,這個地方為啥要弄個抽象類出來?
這個就要結合實際來理解了,分店與分店之間,需不需要統一規範化管理?需不需要保證自己的特色?答案毫無疑問,都是需要的
這個地方製作披薩的過程,毫無疑問是肯定要一致的。就像外賣一樣,下單,炒菜,配送。整套流程都是這樣,不能說你出來就開始炒菜了啊,這不科學。不一樣的地方就是,你炒的什麼菜,好不好吃。配送得快不快,穩不穩,服務好不好。
所以,抽象類的意義就是:規範、特色
2,factory咋個不見了?
因為把它和具體的store合併在一起了,這樣又引申出另外一個問題:為啥要合併?因為store現在充當的角色就是facotry,剛纔說過的製作過程已經放到父類中實現了,現在只需要在具體的store中去解決披薩的創建問題
3,為啥又不用枚舉了,弄個String來創建pizza?
如果還是單機,用枚舉當然會比直接扔個string來得穩當。
開了分店,要是每個分店都是一套完整的服務在玩,丟個string,要比枚舉來得好。原因有2:傳輸過程中的序列化和反序列化、更加靈活(客戶端不用每次都因為這個原因要去升級對應的包,特別是多個版本在跑得時候,升級了又會導致其他東西不能玩)
測試類:
/** * 披薩測試類 * @author skysea */ public class PizzaTest { public static void main(String[] args) { PizzaStore nyStore = new NYPizzaStore(); PizzaStore chicagoStore = new ChicagoPizzaStore(); Pizza pizza = nyStore.orderPizza(NYPizzaStore.CHEESE); System.out.println("Ethan ordered a " + pizza.getName() + "\n"); pizza = chicagoStore.orderPizza(ChicagoPizzaStore.CHEESE); System.out.println("Joel ordered a " + pizza.getName() + "\n"); pizza = nyStore.orderPizza(NYPizzaStore.CLAM); System.out.println("Ethan ordered a " + pizza.getName() + "\n"); pizza = chicagoStore.orderPizza(ChicagoPizzaStore.CLAM); System.out.println("Joel ordered a " + pizza.getName() + "\n"); pizza = nyStore.orderPizza(NYPizzaStore.PEPPERONI); System.out.println("Ethan ordered a " + pizza.getName() + "\n"); pizza = chicagoStore.orderPizza(ChicagoPizzaStore.PEPPERONI); System.out.println("Joel ordered a " + pizza.getName() + "\n"); pizza = nyStore.orderPizza(NYPizzaStore.VEGGIE); System.out.println("Ethan ordered a " + pizza.getName() + "\n"); pizza = chicagoStore.orderPizza(ChicagoPizzaStore.VEGGIE); System.out.println("Joel ordered a " + pizza.getName() + "\n"); } }
結果(結果太多了,就不全部截圖出來了):