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

BWH_Steven發表於2020-11-05

一 為什麼要用工廠模式

之前講解 Spring 的依賴注入的文章時,我們就已經有提到過工廠這種設計模式,我們直接先通過一個例子來看一下究竟工廠模式能用來做什麼?

【萬字長文】Spring框架 層層遞進輕鬆入門 (IOC和DI)

首先,我們簡單的模擬一個對賬戶進行新增的操作,我們先採用我們以前常常使用的方式進行模擬,然後再給出改進方案

(一) 舉一個模擬 Spring IOC 的例子

(1) 以前的程式

首先,按照我們常規的方式先模擬,我們先將一套基本流程走下來

A:Service 層

/**
 * 賬戶業務層介面
 */
public interface AccountService {
    void addAccount();
}

/**
 * 賬戶業務層實現類
 */
public class AccountServiceImpl implements AccountService {
	
	private AccountDao accountDao = new AccountDaoImpl();
	
    public void addAccount() {
        accountDao.addAccount();
    }
}

B:Dao 層

/**
 * 賬戶持久層介面
 */
public interface AccountDao {
    void addAccount();
}

/**
 * 賬戶持久層實現類
 */
public class AccountDaoImpl implements AccountDao {

    public void addAccount() {
        System.out.println("新增使用者成功!");
    }
}

C:呼叫

由於,我們建立的Maven工程並不是一個web工程,我們也只是為了簡單模擬,所以在這裡,建立了一個 Client 類,作為客戶端,來測試我們的方法

public class Client {
    public static void main(String[] args) {
		AccountService  as = new AccountServiceImpl();
		as.addAccount();
    }
}

執行的結果,就是在螢幕上輸出一個新增使用者成功的字樣

D:分析:new 的問題

上面的這段程式碼,應該是比較簡單也容易想到的一種實現方式了,但是它的耦合性卻是很高的,其中這兩句程式碼,就是造成耦合性高的根由,因為業務層(service)呼叫持久層(dao),這個時候業務層將很大的依賴於持久層的介面(AccountDao)和實現類(AccountDaoImpl)

private AccountDao accountDao = new AccountDaoImpl();

AccountService as = new AccountServiceImpl();

這種通過 new 物件的方式,使得不同類之間的依賴性大大增強,其中一個類的問題,就會直接導致出現全域性的問題,如果我們將被呼叫的方法進行錯誤的修改,或者說刪掉某一個類,執行的結果就是:

編譯期就出現了錯誤,而我們作為一個開發者,我們應該努力讓程式在編譯期不依賴,而執行時才可以有一些必要的依賴(依賴是不可能完全消除的)

所以,我們應該想辦法進行解耦,要解耦就要使呼叫者被呼叫者之間沒有什麼直接的聯絡,那麼工廠模式就可以幫助我們很好的解決這個問題

(2) 工廠模式改進

A:BeanFactory

具體怎麼實現呢?在這裡可以將 serivice 和 dao 均配置到配置檔案中去(xml/properties),通過一個類讀取配置檔案中的內容,並使用反射技術建立物件,然後存起來,完成這個操作的類就是我們的工廠

注:在這裡我們使用了 properties ,主要是為了實現方便,xml還涉及到解析的一些程式碼,相對麻煩一些,不過我們下面要說的 Spring 就是使用了 xml做配置檔案

  • bean.properties:先寫好配置檔案,將 service 和 dao 以 key=value 的格式配置好
accountService=cn.ideal.service.impl.AccountServiceImpl
accountDao=cn.ideal.dao.impl.AccountDaoImpl
  • BeanFactory
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值

  • 下一句就是通過 Class 的載入方法載入這個類,例項化後返回
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:測試程式碼:

public class Client {
    public static void main(String[] args) {
        AccountService as = 					   (AccountService)BeanFactory.getBean("accountService");
        as.addAccount();
    }
}

C:執行效果:

當我們按照同樣的操作,刪除掉被呼叫的 dao 的實現類,可以看到,這時候編譯期錯誤已經消失了,而報出來的只是一個執行時異常,這樣就解決了前面所思考的問題

我們應該努力讓程式在編譯期不依賴,而執行時才可以有一些必要的依賴(依賴是不可能完全消除的)

(3) 小總結:

為什麼使用工廠模式替代了 new 的方式?

打個比方,在你的程式中,如果一段時間後,你發現在你 new 的這個物件中存在著bug或者不合理的地方,或者說你甚至想換一個持久層的框架,這種情況下,沒辦法,只能修改原始碼了,然後重新編譯,部署,但是如果你使用工廠模式,你只需要重新將想修改的類,單獨寫好,編譯後放到檔案中去,只需要修改一下配置檔案就可以了

我分享下我個人精簡下的理解就是:

【new 物件依賴的是具體事物,而不 new 則是依賴抽象事物】

Break it down:

  • 依賴具體事物,這個很好理解,你依賴的是一個具體的,實實在在內容,它與你係相關,所以有什麼問題,都是連環的,可能為了某個點,我們需要修改 N 個地方,絕望
  • 依賴抽象事物,你所呼叫的並不是一個直接就可以觸手可及的東西,是一個抽象的概念,所以不存在上面那種情況下的連環反應

二 三種工廠模式

看完前面的例子,我想大家已經已經對工廠模式有了一個非常直觀的認識了

說白了,工廠模式就是使用一種手段,代替了 new 這個操作

以往想要獲取一個例項的時候要 new 出來,但是這種方式耦合性就會很高,我們要儘量的減少這種可避免的耦合負擔,所以工廠模式就來了

工廠就是在呼叫者和被呼叫者之間起一個連線樞紐的作用,呼叫者和被呼叫者都只與工廠進行聯絡,從而減少了兩者之間直接的依賴

工廠模式一共有三種 ① 簡單工廠模式,② 工廠方法模式 ③ 抽象工廠模式

下面我們一個一個來說

(一) 簡單工廠模式

(1) 實現

下面我們以一個車的例子來講,首先我們有一個抽象的 Car 類

public abstract class Car {
    // 任何汽車都會跑
    public abstract void run();
}

接著就是它的子類,我們先來兩個,一個寶馬類,一個賓士類(為閱讀方便寫成了拼音命名,請勿模仿,不建議)

public class BaoMa extends Car {
    @Override
    public void run() {
        System.out.println("【寶馬】在路上跑");
    }
}
public class BenChi extends Car {
    @Override
    public void run() {
        System.out.println("【賓士】在路上跑");
    }
}

那如果我想要例項化這個類,實際上最原始的寫法可以這樣(也就是直接 new 出來)

public class Test {
    public static void main(String[] args) {
        Car baoMa = new BaoMa();
        baoMa.run();
        Car benChi = new BenChi();
        benChi.run();
    }
}

如果使用簡單工廠模式,就需要建立一個專門的工廠類,用來例項化物件

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 建立出想要的東西就可以了,具體怎麼去建立就不需要呼叫者操心了

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) 實現

依舊是一個汽車抽象類,一個寶馬類和一個賓士類是其子類

public abstract class Car {
    // 任何汽車都會跑
    public abstract void run();
}
public class BaoMa extends Car {
    @Override
    public void run() {
        System.out.println("【寶馬】在路上跑");
    }
}
public class BenChi extends Car {
    @Override
    public void run() {
        System.out.println("【賓士】在路上跑");
    }
}

如果是簡單工廠類,就會 有一個總的工廠類來例項化物件,為了解決其缺點,工廠類首先需要建立一個汽車工廠介面類

public interface CarFactory {
    // 可以獲取任何車
    Car createCar();
}

然後寶馬和賓士類分別實現它,內容就是建立一個對應寶馬或者賓士(例項化寶馬類或者賓士類)

public class BaoMaFactory implements CarFactory {
    @Override
    public Car createCar() {
        return new BaoMa();
    }
}
public class BenChiFactory implements CarFactory {
    @Override
    public Car createCar() {
        return new BenChi();
    }
}

想要獲取車的時候,只需要通過多型建立出想要獲得的那種車的工廠,然後通過工廠再建立出對應的車,例如我分別拿到賓士和寶馬就可以這樣做:

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)以及對應保時捷工廠類後後,仍只需要通過以上方法呼叫即可

// 先去保時捷工廠拿到一臺保時捷
CarFactory baoShiJieFactory = new BaoShiJieFactory();
// 4S店拿到一臺保時捷,給了你
Car baoShiJie = baoShiJieFactory.createCar();
baoShiJie.run();

(2) 定義

工廠方法模式:定義一個用於建立物件的介面,讓子類決定例項化哪一個類,工廠方法使一個類的例項化延遲到其子類

看其結構圖

(3) 優缺點

優點:

  • 物件的建立,被明確到了各個子工廠類中,不再需要在客戶端中考慮

  • 新內容增加非常方便,只需要增加一個想生成的類和建立其的工廠類

  • 不違背 “開閉原則”,後期維護,擴充套件方便

缺點:

  • 程式碼量顯著增加

(三) 抽象工廠模式

抽象工廠模式是一種比較複雜的工廠模式,下面先直接通過程式碼瞭解一下

還是說車,我們將車分為兩種,一種是普通轎車,一種是卡車,前面的工廠方法模式中,如果不斷的增加車的型別,這勢必會造成工廠過多,但是對於常見的車來說,還可以尋找可抽取的特點,來進行抽象

所以在此基礎之上,我們又分別設定了自動擋和手動擋兩種型別,所以兩兩搭配,就有四種情況了(eg:自動擋卡車,手動擋轎車等等)

(1) 建立抽象產品

  • 首先分別建立普通轎車和卡車的抽象類,然後定義兩個方法(這裡我就寫成一樣的了,可以根據轎車和卡車的特點寫不同的方法)
public abstract class CommonCar {
    // 所有車都能,停車
    abstract void parking();
    // 所有車都能,換擋
    abstract void shiftGear();
}
public abstract class Truck  {
    // 所有車都能,停車
    abstract void parking();
    // 所有車都能,換擋
    abstract void shiftGear();
}

(2) 實現抽象產品

說明: A是自動的意思,H是手動的意思,eg:CommonCarA 代表普通自動擋轎車

  • 實現抽象產品——小轎車(自動擋)
public class CommonCarA extends CommonCar{
    @Override
    void parking() {
        System.out.println("自動擋轎車A,停車掛P檔");
    }

    @Override
    void shiftGear() {
        System.out.println("自動擋轎車A,可換擋 P N D R");
    }
}
  • 實現抽象產品——小轎車(手動擋)
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");
    }
}
  • 實現抽象產品——貨車(自動擋)
public class TruckA extends Truck {
    @Override
    void parking() {
        System.out.println("自動擋貨車A,停車掛P檔");
    }

    @Override
    void shiftGear() {
        System.out.println("自動擋貨車A,可換擋 P N D R");
    }
}
  • 實現抽象產品——貨車(手動擋)
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) 建立抽象工廠

public interface CarFactory {
    // 建立普通轎車
    CommonCar createCommonCar();
    // 建立貨車
    Truck createTruckCar();
}

(4) 實現抽象工廠

通過自動擋手動擋這兩個抽象概念,建立出這兩個工廠,建立具有特定實現類的產品物件

  • 自動擋汽車工廠類
public class AutomaticCarFactory implements CarFactory {
    @Override
    public CommonCarA createCommonCar() {
        return new CommonCarA();
    }

    @Override
    public TruckA createTruckCar() {
        return new TruckA();
    }
}
  • 手動擋汽車工廠類
public class HandShiftCarFactory implements CarFactory {
    @Override
    public CommonCarH createCommonCar() {
        return new CommonCarH();
    }

    @Override
    public TruckH createTruckCar() {
        return new TruckH();
    }
}

(5) 測試一下

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(自動擋汽車工廠)這個工廠生成的

(6) 結構圖

看著結構圖,我們再捋一下

首先 AbstractProductA 和 AbstractProductB 是兩個抽象產品,分別對應我們上述程式碼中的 CommonCar 和 Truck,為什麼是抽象的,因為它們可以都有兩種不同的實現,即自動擋轎車和自動貨車,手動擋轎車和手動擋卡車

ProductA1 和 ProductA2 和 ProductB1 和 ProductB2 就是具體的實現,代表 CommonCarA 和 CommonCarH 和 TruckA 和 TruckH

抽象工廠 AbstractFactory 裡包含了所有產品建立的抽象方法,ConcreteFactory1 和 ConcreteFactory2 就是具體的工廠,通常是在執行時再建立一個 ConcreteFactory 的例項,這個工廠再建立具有特定實現的產品物件,也就是說為了建立不同的產品物件,客戶端應該使用不同的具體工廠

(7) 反射+配置檔案實現優化

抽象工廠說白了就是通過內容抽象的方式,減少了工廠的數量,同時在具體工廠我們可以這麼用

CarFactory factory = new AutomaticCarFactory();

具體工廠只需要在初始化的時候出現一次,這也使得修改一個具體工廠也是比較容易的

但是缺點也是非常明顯,當我想擴充套件一,比如加一個拖拉機型別,我就需要修改 CarFactory介面,AutomaticCarFactory 類 HandShiftCarFactory 類,(當然,拖拉機貌似沒有什麼自動擋,我只是為了舉例子),還需要增加拖拉機對應的內容

也就是說,增加的基礎上,我還需要修改原先的三個類,這是一個非常顯著的缺點

除此之外還有一個問題,如果很多地方都宣告瞭

CarFactory factory = new AutomaticCarFactory();

並且進行了呼叫,如果我更換了這個工廠,就需要大量的進行修改,很顯然這一點是有問題的,我們下面來使用反射優化一下

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

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 方法,就不需要修改多個內容了

相關文章