一篇搞定工廠模式【簡單工廠、工廠方法模式、抽象工廠模式】
阿新 • • 發佈:2020-11-05
![](//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` 方法,就不需要修改多個內容了