1. 程式人生 > >一篇搞定工廠模式【簡單工廠、工廠方法模式、抽象工廠模式】

一篇搞定工廠模式【簡單工廠、工廠方法模式、抽象工廠模式】

![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3c419f4402ad40be9539f85b636d67a6~tplv-k3u1fbpfcp-zoom-1.image) # 一 為什麼要用工廠模式 之前講解 Spring 的依賴注入的文章時,我們就已經有提到過工廠這種設計模式,我們直接先通過一個例子來看一下究竟工廠模式能用來做什麼? 【萬字長文】Spring框架 層層遞進輕鬆入門 (IOC和DI) 首先,我們簡單的模擬一個對賬戶進行新增的操作,我們先採用我們以前常常使用的方式進行模擬,然後再給出改進方案 ## (一) 舉一個模擬 Spring IOC 的例子 ### (1) 以前的程式 首先,按照我們常規的方式先模擬,我們先將一套基本流程走下來 #### A:Service 層 ```java /** * 賬戶業務層介面 */ public interface AccountService { void addAccount(); } /** * 賬戶業務層實現類 */ public class AccountServiceImpl implements AccountService { private AccountDao accountDao = new AccountDaoImpl(); public void addAccount() { accountDao.addAccount(); } } ``` #### B:Dao 層 ```java /** * 賬戶持久層介面 */ public interface AccountDao { void addAccount(); } /** * 賬戶持久層實現類 */ public class AccountDaoImpl implements AccountDao { public void addAccount() { System.out.println("新增使用者成功!"); } } ``` #### C:呼叫 由於,我們建立的Maven工程並不是一個web工程,我們也只是為了簡單模擬,所以在這裡,建立了一個 Client 類,作為客戶端,來測試我們的方法 ```java public class Client { public static void main(String[] args) { AccountService as = new AccountServiceImpl(); as.addAccount(); } } ``` 執行的結果,就是在螢幕上輸出一個新增使用者成功的字樣 #### D:分析:new 的問題 上面的這段程式碼,應該是比較簡單也容易想到的一種實現方式了,但是它的耦合性卻是很高的,其中這兩句程式碼,就是造成耦合性高的根由,因為業務層(service)呼叫持久層(dao),這個時候業務層將很大的依賴於持久層的介面(AccountDao)和實現類(AccountDaoImpl) ```java private AccountDao accountDao = new AccountDaoImpl(); AccountService as = new AccountServiceImpl(); ``` 這種通過 new 物件的方式,使得不同類之間的依賴性大大增強,其中一個類的問題,就會直接導致出現全域性的問題,如果我們將被呼叫的方法進行錯誤的修改,或者說刪掉某一個類,執行的結果就是: ![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1157401e7db24e159882caed3fc2276e~tplv-k3u1fbpfcp-zoom-1.image) 在**編譯期**就出現了**錯誤**,而我們作為一個開發者,我們應該努力讓程式在編譯期不依賴,而執行時才可以有一些必要的依賴(依賴是不可能完全消除的) 所以,我們應該想辦法進行**解耦**,要解耦就要使**呼叫者**和**被呼叫者**之間沒有什麼直接的聯絡,那麼**工廠模式**就可以幫助我們很好的解決這個問題 ### (2) 工廠模式改進 #### A:BeanFactory 具體怎麼實現呢?在這裡可以將 serivice 和 dao 均配置到配置檔案中去(xml/properties),通過一個類讀取配置檔案中的內容,並使用反射技術建立物件,然後**存起來**,完成這個操作的類就是我們的工廠 注:在這裡我們使用了 properties ,主要是為了實現方便,xml還涉及到解析的一些程式碼,相對麻煩一些,不過我們下面要說的 Spring 就是使用了 xml做配置檔案 - bean.properties:先寫好配置檔案,將 service 和 dao 以 key=value 的格式配置好 ```properties accountService=cn.ideal.service.impl.AccountServiceImpl accountDao=cn.ideal.dao.impl.AccountDaoImpl ``` - BeanFactory ```java public class BeanFactory { //定義一個Properties物件 private static Properties properties; //使用靜態程式碼塊為Properties物件賦值 static { try{ //例項化物件 properties = new Properties(); //獲取properties檔案的流物件 InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"); properties.load(in); }catch (Exception e){ throw new ExceptionInInitializerError("初始化properties失敗"); } } } ``` 簡單的解釋一下這部分程式碼(當然還沒寫完):首先就是要將配置檔案中的內容讀入,這裡通過類載入器的方式操作,讀入一個流檔案,然後從中讀取鍵值對,由於只需要執一次,所以放在靜態程式碼塊中,又因為 properties 物件在後面的方法中還要用,所以寫在成員的位置 接著在 BeanFactory 中繼續編寫一個 getBean 方法其中有兩句核心程式碼的意義就是: - 通過方法引數中傳入的字串,找到對應的全類名路徑,實際上也就是通過剛才獲取到的配置內容,通過key 找到 value值 ![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9147e9d71966465f91b3d684e62987e9~tplv-k3u1fbpfcp-zoom-1.image) - 下一句就是通過 Class 的載入方法載入這個類,例項化後返回 ```java public static Object getBean(String beanName){ Object bean = null; try { //根據key獲取value String beanPath = properties.getProperty(beanName); bean = Class.forName(beanPath).newInstance(); }catch (Exception e){ e.printStackTrace(); } return bean; } ``` #### B:測試程式碼: ```java public class Client { public static void main(String[] args) { AccountService as = (AccountService)BeanFactory.getBean("accountService"); as.addAccount(); } } ``` #### C:執行效果: 當我們按照同樣的操作,刪除掉被呼叫的 dao 的實現類,可以看到,這時候編譯期錯誤已經消失了,而報出來的只是一個執行時異常,這樣就解決了前面所思考的問題 >我們應該努力讓程式在編譯期不依賴,而執行時才可以有一些必要的依賴(依賴是不可能完全消除的) ![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6c54d0dd617d4248ac6b3c9d4b82a1a1~tplv-k3u1fbpfcp-zoom-1.image) ### (3) 小總結: **為什麼使用工廠模式替代了 new 的方式?** 打個比方,在你的程式中,如果一段時間後,你發現在你 new 的這個物件中存在著bug或者不合理的地方,或者說你甚至想換一個持久層的框架,這種情況下,沒辦法,只能修改原始碼了,然後重新編譯,部署,但是如果你使用工廠模式,你只需要重新將想修改的類,單獨寫好,編譯後放到檔案中去,只需要修改一下配置檔案就可以了 我分享下我個人精簡下的理解就是: **【new 物件依賴的是具體事物,而不 new 則是依賴抽象事物】** Break it down: - 依賴具體事物,這個很好理解,你依賴的是一個具體的,實實在在內容,它與你係相關,所以有什麼問題,都是連環的,可能為了某個點,我們需要修改 N 個地方,絕望 - 依賴抽象事物,你所呼叫的並不是一個直接就可以觸手可及的東西,是一個抽象的概念,所以不存在上面那種情況下的連環反應 # 二 三種工廠模式 看完前面的例子,我想大家已經已經對工廠模式有了一個非常直觀的認識了 說白了,工廠模式就是使用一種手段,代替了 new 這個操作 以往想要獲取一個例項的時候要 new 出來,但是這種方式耦合性就會很高,我們要儘量的減少這種可避免的耦合負擔,所以工廠模式就來了 > 工廠就是在呼叫者和被呼叫者之間起一個連線樞紐的作用,呼叫者和被呼叫者都只與工廠進行聯絡,從而減少了兩者之間直接的依賴 工廠模式一共有三種 ① 簡單工廠模式,② 工廠方法模式 ③ 抽象工廠模式 下面我們一個一個來說 ## (一) 簡單工廠模式 ### (1) 實現 下面我們以一個車的例子來講,首先我們有一個抽象的 Car 類 ```java public abstract class Car { // 任何汽車都會跑 public abstract void run(); } ``` 接著就是它的子類,我們先來兩個,一個寶馬類,一個賓士類(為閱讀方便寫成了拼音命名,請勿模仿,不建議) ```java public class BaoMa extends Car { @Override public void run() { System.out.println("【寶馬】在路上跑"); } } ``` ```java public class BenChi extends Car { @Override public void run() { System.out.println("【賓士】在路上跑"); } } ``` 那如果我想要例項化這個類,實際上最原始的寫法可以這樣(也就是直接 new 出來) ```java public class Test { public static void main(String[] args) { Car baoMa = new BaoMa(); baoMa.run(); Car benChi = new BenChi(); benChi.run(); } } ``` 如果使用簡單工廠模式,就需要建立一個專門的工廠類,用來例項化物件 ```java public class CarFactory { public static Car createCar(String type) { if ("寶馬".equals(type)) { return new BaoMa(); } else if ("賓士".equals(type)) { return new BenChi(); } else { return null; } } } ``` 真正去呼叫的時候,我只需要傳入一個正確的引數,通過 CarFactory 創建出想要的東西就可以了,具體怎麼去建立就不需要呼叫者操心了 ```java public class Test { public static void main(String[] args) { Car baoMa = CarFactory.createCar("寶馬"); baoMa.run(); Car benChi = CarFactory.createCar("賓士"); benChi.run(); } } ``` ### (2) 優缺點 **先說一下優點**: 簡單工廠模式的優點就在於其工廠類中含有必要的邏輯判斷(例如 CarFactory 中判斷是寶馬還是賓士),客戶端只需要通過傳入引數(例如傳入 “寶馬”),動態的例項化想要的類,客戶端就免去了直接建立產品的職責,去除了與具體產品的依賴(都不需要知道具體類名了,反正我不負責建立) **但是其缺點也很明顯**: 簡單工廠模式的工廠類職責過於繁重,違背了高聚合原則,同時其內容多的情況下,邏輯太複雜。最關鍵的是,當我想要增加一個新的內容的時候,例如增加一個保時捷,我就不得不去修改 CarFactory 工廠類中的程式碼,這很顯然違背了 “開閉原則” 所以,工廠模式他就來了 ## (二) 工廠模式 ### (1) 實現 依舊是一個汽車抽象類,一個寶馬類和一個賓士類是其子類 ```java public abstract class Car { // 任何汽車都會跑 public abstract void run(); } ``` ```java public class BaoMa extends Car { @Override public void run() { System.out.println("【寶馬】在路上跑"); } } ``` ```java public class BenChi extends Car { @Override public void run() { System.out.println("【賓士】在路上跑"); } } ``` 如果是簡單工廠類,就會 有一個總的工廠類來例項化物件,為了解決其缺點,工廠類首先需要建立一個汽車工廠介面類 ```java public interface CarFactory { // 可以獲取任何車 Car createCar(); } ``` 然後寶馬和賓士類分別實現它,內容就是建立一個對應寶馬或者賓士(例項化寶馬類或者賓士類) ```java public class BaoMaFactory implements CarFactory { @Override public Car createCar() { return new BaoMa(); } } ``` ```java public class BenChiFactory implements CarFactory { @Override public Car createCar() { return new BenChi(); } } ``` 想要獲取車的時候,只需要通過多型創建出想要獲得的那種車的工廠,然後通過工廠再創建出對應的車,例如我分別拿到賓士和寶馬就可以這樣做: ```java public class Test { public static void main(String[] args) { // 先去賓士工廠拿到一臺賓士 CarFactory benChiFactory = new BenChiFactory(); // 4S店拿到一臺賓士,給了你 Car benChi = benChiFactory.createCar(); benChi.run(); // 先去寶馬工廠拿到一臺寶馬 CarFactory baoMaFactory = new BaoMaFactory(); // 4S店拿到一臺寶馬,給了你 Car baoMa = baoMaFactory.createCar(); baoMa.run(); } } ``` 這種情況下,如果我還想要增加一臺保時捷型別的車,創建出對應的保時捷類(繼承 Car)以及對應保時捷工廠類後後,仍只需要通過以上方法呼叫即可 ```java // 先去保時捷工廠拿到一臺保時捷 CarFactory baoShiJieFactory = new BaoShiJieFactory(); // 4S店拿到一臺保時捷,給了你 Car baoShiJie = baoShiJieFactory.createCar(); baoShiJie.run(); ``` ### (2) 定義 **工廠方法模式:定義一個用於建立物件的介面,讓子類決定例項化哪一個類,工廠方法使一個類的例項化延遲到其子類** 看其結構圖 ![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1f3294ed979c4239b17b4e63537f237b~tplv-k3u1fbpfcp-zoom-1.image) ### (3) 優缺點 優點: - 物件的建立,被明確到了各個子工廠類中,不再需要在客戶端中考慮 - 新內容增加非常方便,只需要增加一個想生成的類和建立其的工廠類 - 不違背 “開閉原則”,後期維護,擴充套件方便 缺點: - 程式碼量顯著增加 ## (三) 抽象工廠模式 抽象工廠模式是一種比較複雜的工廠模式,下面先直接通過程式碼瞭解一下 還是說車,我們將車分為兩種,一種是普通轎車,一種是卡車,前面的工廠方法模式中,如果不斷的增加車的型別,這勢必會造成工廠過多,但是對於常見的車來說,還可以尋找可抽取的特點,來進行抽象 所以在此基礎之上,我們又分別設定了自動擋和手動擋兩種型別,所以兩兩搭配,就有四種情況了(eg:自動擋卡車,手動擋轎車等等) ### (1) 建立抽象產品 - 首先分別建立普通轎車和卡車的抽象類,然後定義兩個方法(這裡我就寫成一樣的了,可以根據轎車和卡車的特點寫不同的方法) ```java public abstract class CommonCar { // 所有車都能,停車 abstract void parking(); // 所有車都能,換擋 abstract void shiftGear(); } ``` ```java public abstract class Truck { // 所有車都能,停車 abstract void parking(); // 所有車都能,換擋 abstract void shiftGear(); } ``` ### (2) 實現抽象產品 說明: A是自動的意思,H是手動的意思,eg:CommonCarA 代表普通自動擋轎車 - 實現抽象產品——小轎車(自動擋) ```java public class CommonCarA extends CommonCar{ @Override void parking() { System.out.println("自動擋轎車A,停車掛P檔"); } @Override void shiftGear() { System.out.println("自動擋轎車A,可換擋 P N D R"); } } ``` - 實現抽象產品——小轎車(手動擋) ```java public class CommonCarH extends CommonCar { @Override void parking() { System.out.println("手動擋轎車H,停車掛空擋,拉手剎"); } @Override void shiftGear() { System.out.println("手動擋轎車H,可換擋 空 1 2 3 4 5 R"); } } ``` - 實現抽象產品——貨車(自動擋) ```java public class TruckA extends Truck { @Override void parking() { System.out.println("自動擋貨車A,停車掛P檔"); } @Override void shiftGear() { System.out.println("自動擋貨車A,可換擋 P N D R"); } } ``` - 實現抽象產品——貨車(手動擋) ```java public class TruckH extends Truck { @Override void parking() { System.out.println("手動檔貨車H,停車掛空擋,拉手剎"); } @Override void shiftGear() { System.out.println("手動檔貨車H,可換擋 空 1 2 3 4 5 R"); } } ``` ### (3) 建立抽象工廠 ```java public interface CarFactory { // 建立普通轎車 CommonCar createCommonCar(); // 建立貨車 Truck createTruckCar(); } ``` ### (4) 實現抽象工廠 通過自動擋手動擋這兩個抽象概念,創建出這兩個工廠,建立具有特定實現類的產品物件 - 自動擋汽車工廠類 ```java public class AutomaticCarFactory implements CarFactory { @Override public CommonCarA createCommonCar() { return new CommonCarA(); } @Override public TruckA createTruckCar() { return new TruckA(); } } ``` - 手動擋汽車工廠類 ```java public class HandShiftCarFactory implements CarFactory { @Override public CommonCarH createCommonCar() { return new CommonCarH(); } @Override public TruckH createTruckCar() { return new TruckH(); } } ``` ### (5) 測試一下 ```java public class Test { public static void main(String[] args) { // 自動擋車工廠類 CarFactory automaticCarFactory = new AutomaticCarFactory(); // 手動擋車工廠類 CarFactory handShiftCarFactory = new HandShiftCarFactory(); System.out.println("=======自動擋轎車系列======="); CommonCar commonCarA = automaticCarFactory.createCommonCar(); commonCarA.parking(); commonCarA.shiftGear(); System.out.println("=======自動擋貨車系列======="); Truck truckA = automaticCarFactory.createTruckCar(); truckA.parking(); truckA.shiftGear(); System.out.println("=======手動擋轎車系列======="); CommonCar commonCarH = handShiftCarFactory.createCommonCar(); commonCarH.parking(); commonCarH.shiftGear(); System.out.println("=======手動擋貨車系列======="); Truck truckH = handShiftCarFactory.createTruckCar(); truckH.parking(); truckH.shiftGear(); } } ``` 執行結果: ``` =======自動擋轎車系列======= 自動擋轎車A,停車掛P檔 自動擋轎車A,可換擋 P N D R =======自動擋貨車系列======= 自動擋貨車A,停車掛P檔 自動擋貨車A,可換擋 P N D R =======手動擋轎車系列======= 手動擋轎車H,停車掛空擋,拉手剎 手動擋轎車H,可換擋 空 1 2 3 4 5 R =======手動擋貨車系列======= 手動檔貨車H,停車掛空擋,拉手剎 手動檔貨車H,可換擋 空 1 2 3 4 5 R ``` 補充兩個概念 - **產品等級結構**:**產品的等級結構就是其繼承結構**,例如上述程式碼中,CommonCar(普通轎車) 是一個抽象類,其子類有 CommonCarA (自動擋轎車)和 CommonCarH(手動擋轎車),則 **普通轎車抽象類**就與具體**自動擋**或者手動擋的轎車構成一個產品等級結構。 - **產品族**:**產品族是同一個工廠生產,位於不同產品等級結構中的一組產品**,例如上述程式碼中,CommonCarA(自動擋轎車)和 TruckA(自動擋貨車),都是AutomaticCarFactory(自動擋汽車工廠)這個工廠生成的 ![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/de2a14632a2142cc830a09f1f86ad796~tplv-k3u1fbpfcp-zoom-1.image) ### (6) 結構圖 ![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4594cf8bf94548fc80e2368216d18972~tplv-k3u1fbpfcp-zoom-1.image) 看著結構圖,我們再捋一下 首先 AbstractProductA 和 AbstractProductB 是兩個抽象產品,分別對應我們上述程式碼中的 CommonCar 和 Truck,為什麼是抽象的,因為它們可以都有兩種不同的實現,即自動擋轎車和自動貨車,手動擋轎車和手動擋卡車 ProductA1 和 ProductA2 和 ProductB1 和 ProductB2 就是具體的實現,代表 CommonCarA 和 CommonCarH 和 TruckA 和 TruckH 抽象工廠 AbstractFactory 裡包含了所有產品建立的抽象方法,ConcreteFactory1 和 ConcreteFactory2 就是具體的工廠,通常是在執行時再建立一個 ConcreteFactory 的例項,這個工廠再建立具有特定實現的產品物件,也就是說為了建立不同的產品物件,客戶端應該使用不同的具體工廠 ### (7) 反射+配置檔案實現優化 抽象工廠說白了就是通過內容抽象的方式,減少了工廠的數量,同時在具體工廠我們可以這麼用 ```java CarFactory factory = new AutomaticCarFactory(); ``` 具體工廠只需要在初始化的時候出現一次,這也使得修改一個具體工廠也是比較容易的 但是缺點也是非常明顯,當我想擴充套件一,比如加一個拖拉機型別,我就需要修改 CarFactory介面,AutomaticCarFactory 類 HandShiftCarFactory 類,(當然,拖拉機貌似沒有什麼自動擋,我只是為了舉例子),還需要增加拖拉機對應的內容 也就是說,增加的基礎上,我還需要修改原先的三個類,這是一個非常顯著的缺點 除此之外還有一個問題,如果很多地方都聲明瞭 ```Jjava CarFactory factory = new AutomaticCarFactory(); ``` 並且進行了呼叫,如果我更換了這個工廠,就需要大量的進行修改,很顯然這一點是有問題的,我們下面來使用反射優化一下 ```java public class Test { public static void main(String[] args) throws Exception { Properties properties = new Properties(); // 使用ClassLoader載入properties配置檔案生成對應的輸入流 InputStream in = Test.class.getClassLoader().getResourceAsStream("config.properties"); // 使用properties物件載入輸入流 properties.load(in); //獲取key對應的value值 String factory = properties.getProperty("factory"); CarFactory automaticCarFactory = (CarFactory) Class.forName(factory).newInstance(); System.out.println("======轎車系列======="); CommonCar commonCarA = automaticCarFactory.createCommonCar(); commonCarA.parking(); commonCarA.shiftGear(); System.out.println("=======貨車系列======="); Truck truckA = automaticCarFactory.createTruckCar(); truckA.parking(); truckA.shiftGear(); } } ``` config.properties ```properties factory=cn.ideal.factory.abstractFactory.AutomaticCarFactory #factory=cn.ideal.factory.abstractFactory.HandShiftCarFactory ``` 執行結果: ``` =======轎車系列======= 自動擋轎車A,停車掛P檔 自動擋轎車A,可換擋 P N D R =======貨車系列======= 自動擋貨車A,停車掛P檔 自動擋貨車A,可換擋 P N D R ``` **通過反射+配置檔案我們就可以使得使用配置檔案中的鍵值對(字串)來例項化物件,而變數是可以更換的,也就是說程式由編譯時轉為執行時,增大了靈活性,去除了判斷的麻煩** 回到前面的問題,如果我們現在要增加一個新的內容,內容的增加沒什麼好說的,這是必須的,這是擴充套件,但是對於修改我們卻要儘量關閉,現在我們可以通過修改配置檔案來達到例項化不同具體工廠的方式 但是還需要修改三個類,以新增新內容,這裡還可以通過簡單工廠來進行優化,也就是去掉這幾個工廠,使用一個簡單工廠,其中寫入`createCommonCar();` 等這些方法, 再配合反射+配置檔案也能實現剛才的效果,這樣如果新增內容的時候只需要修改配置檔案後,再修改這一個類就可以,即增加一個 `createXXX` 方法,就不需要修改多個內容了