工廠模式-將物件的建立封裝起來

碼農充電站發表於2020-12-25

公號:碼農充電站pro
主頁:https://codeshellme.github.io

工廠模式(Factory Design Pattern)可細分為三種,分別是簡單工廠工廠方法抽象工廠,它們都是為了更好的建立物件。

所謂的“工廠”,就是用來將建立物件的程式碼封裝起來,因為這部分程式碼將來變動的機率很大,所以這裡的“工廠”的實質作用就是“封裝變化”,以便於維護。

其中用到了“針對介面程式設計,而非針對實現程式設計”的設計原則。

下面通過一個銷售飲料的例子,來對這三種工廠模式進行介紹。

1,簡單工廠模式

假如現在有一位老闆,想開一家飲料店,該店銷售各種口味的飲料,比如蘋果味,香蕉味,橙子味等。

將這些飲料用程式碼來表示,如下:

class Drink {
    public void packing() {
        //
    }
}

class DrinkApple extends Drink {
}

class DrinkBanana extends Drink {
}

class DrinkOrange extends Drink {
}

Drink 類為所有其它味道的飲料的父類。

當有顧客來購買飲料的時候,顧客需要說明想買哪種口味的飲料,然後服務員就去將該口味的飲料取過來,包裝好,然後交給顧客。

下面我們用最簡單直接的程式碼,來模擬飲料店和賣飲料的過程,如下:

class DrinkStore {

    public Drink sellDrink(String flavor) {
        Drink drink;

        if (flavor.equals("apple")) {
            drink = new DrinkApple();
        } else if (flavor.equals("banana")) {
            drink = new DrinkBanana();
        } else if (flavor.equals("orange")) {
            drink = new DrinkOrange();
        } else {
            drink = new Drink();
        }

        drink.packing();

        return drink;
    }
}

但是這種實現方式有個問題,就是當需要下架舊飲料或上架新飲料的時候,會導致下面這部分程式碼被頻繁的修改:

if (flavor.equals("apple")) {
    drink = new DrinkApple();
} else if (flavor.equals("banana")) {
    drink = new DrinkBanana();
} else if (flavor.equals("orange")) {
    drink = new DrinkOrange();
} else {
    drink = new Drink();
}

那這就違背了設計原則中的開閉原則程式碼應該對擴充套件開發,對修改關閉。所以我們需要對該程式碼進行改進,那如何修改呢?

簡單工廠模式告訴我們要將類似這樣的程式碼封裝到一個裡邊,這個類就叫做簡單工廠類,該類中提供一個方法,它可以生產我們所需要的各種物件。

下面用程式碼來模擬這個簡單工廠類,如下:

class SimpleDrinkFactory {
    public Drink createDrink(String flavor) {
        Drink drink;

        // 這段容易被頻繁修改的程式碼,被封裝到了工廠類中
        if (flavor.equals("apple")) {
            drink = new DrinkApple();
        } else if (flavor.equals("banana")) {
            drink = new DrinkBanana();
        } else if (flavor.equals("orange")) {
            drink = new DrinkOrange();
        } else {
            drink = new Drink();
        }

        return drink;
    }
}

可以看到,createDrink 方法完成了建立 Drink 的任務。

createDrink 方法也可以定義成靜態方法,優點是在使用 createDrink 方法時不需要再建立物件,缺點是不能再通過繼承的方式來改變 createDrink 方法的行為。

下面來看如何使用 SimpleDrinkFactory 類:

class DrinkStore {
    private SimpleDrinkFactory factory;

    public DrinkStore(SimpleDrinkFactory factory) {
        this.factory = factory;
    }

    public Drink sellDrink(String flavor) {
        Drink drink = factory.createDrink(flavor);

        drink.packing();

        return drink;
    }
}

可以看到,我們將 SimpleDrinkFactory 類的物件作為 DrinkStore 類的一個屬性,經過改進後的 sellDrink 方法就不需要再被頻繁修改了。如果再需要上架下架飲料,則去修改簡單工廠類 SimpleDrinkFactory 即可。

我將完整的簡單工廠程式碼放在了這裡,供大家參考,類圖如下:

在這裡插入圖片描述

2,工廠方法模式

簡單工廠模式從嚴格意義來說並不是一個設計模式,而更像一種程式設計習慣。

工廠方法模式定義了一個建立物件的介面(該介面是一個抽象方法,也叫做“工廠方法”),但由子類(實現抽象方法)決定要例項化的類是哪個。

在工廠方法模式中,父類並不關心子類的具體實現,但是父類給了子類一個“規範”,讓子類必須“生成”父類想要的東西。

工廠方法將類的例項化推遲到了子類中,讓子類來控制例項化的細節,也就是將建立物件的過程封裝了起來。而真正使用例項的是父類,這樣就將例項的“實現”從“使用”中解耦出來。因此,工廠方法模式比簡單工廠模式更加有“彈性”。

下面我們來看下如何用工廠方法模式來改進上面的程式碼。

首先,定義 DrinkStoreAbstract ,它是一個抽象父類:

abstract class DrinkStoreAbstract {

    // final 防止子類覆蓋
    public final Drink sellDrink(String flavor) {

        Drink drink = factoryMethod(flavor);    // 使用例項

        drink.packing();

        return drink;
    }

    // 子類必須實現
    protected abstract Drink factoryMethod(String flavor);
}

上面程式碼中 factoryMethod 方法就是所謂的工廠方法,它是一個抽象方法,子類必須實現該方法。

factoryMethod 方法負責生成物件,使用物件的是父類,而實際生成物件的則是子類。

接下來定義一個具體的 DrinkStore 類,它是 DrinkStoreAbstract 的子類:

class DrinkStore extends DrinkStoreAbstract {

    @Override
    public Drink factoryMethod(String flavor) {
        Drink drink;

        if (flavor.equals("apple")) {
            drink = new DrinkApple();
        } else if (flavor.equals("banana")) {
            drink = new DrinkBanana();
        } else if (flavor.equals("orange")) {
            drink = new DrinkOrange();
        } else {
            drink = new Drink();
        }

        return drink;
    }
}

可以看到,子類中的 factoryMethod 方法有了具體的實現。

如果需要上架下架飲料,則去修改子類中的工廠方法 factoryMethod 即可。而 DrinkStoreAbstract 作為一個“框架”,無須改動。

完整的工廠方法程式碼放在了這裡,供大家參考,類圖如下:

在這裡插入圖片描述

圖中的粉色區域是工廠方法模式的重點關注區。

3,依賴倒置原則

在介紹抽象工廠模式之前,我們先來看看什麼是依賴倒置原則

依賴倒置包含了依賴倒置兩個詞。

我們先來看看“依賴”,“依賴”是指類與類之間的依賴關係。

在本文剛開始時的 sellDrink 方法是這麼寫的:

public Drink sellDrink(String flavor) {
      Drink drink;

      if (flavor.equals("apple")) {
          drink = new DrinkApple();
      } else if (flavor.equals("banana")) {
          drink = new DrinkBanana();
      } else if (flavor.equals("orange")) {
          drink = new DrinkOrange();
      } else {
          drink = new Drink();
      }

      drink.packing();

      return drink;
  }

sellDrink 方法依賴了三個具體類,如果飲料的味道繼續增加的話,那麼 sellDrink 方法將依賴更多的具體類

這會導致只要任意一個具體類發生改變,sellDrink 方法就不得不去改變,也就是類 A 需要改變(A 也是一個具體類)。

在這裡插入圖片描述

在上面這個關係圖中,A 稱為高層元件,各個具體類稱為低層元件,所以在這幅圖中,高層元件依賴了低層元件。

依賴倒置原則是指“要依賴抽象,而不依賴具體類”。更具體來說就是,高層元件不應該依賴低層元件,高層元件和低層元件都應該依賴抽象類

那麼怎樣才能達到依賴倒置原則呢?工廠方法就可以!

在經過工廠方法模式的改造之後,最終的 DrinkStoreAbstract 類中的 sellDrink 方法變成了下面這樣:

public final Drink sellDrink(String flavor) {

      Drink drink = factoryMethod(flavor);    // 使用例項

      drink.packing();

      return drink;
  }

這使得 sellDrink 的所在類不再依賴於具體類,而依賴於一個抽象方法 factoryMethod,而 factoryMethod 方法依賴於 Drink,然後,各個具體類也依賴於 DrinkDrink 是一個廣義上的“抽象介面”。

這樣,高層元件和低層元件都依賴於 Drink 抽象,關係圖變成了下面這樣:

在這裡插入圖片描述

之前各個具體類的箭頭是向下指的,而現在各個具體類的箭頭是向上指的,箭頭的方向倒了過來,這就是所謂的依賴倒置

依賴倒置原則使得高層元件和低層元件都依賴於同一個抽象。

那怎樣才能避免違反依賴倒置原則呢?有下面三個指導方針:

  • 變數不要持有具體類的引用。
    • 需要 new 物件的地方,要改為工廠方法。
  • 類不要派生自具體類,而要從抽象類或介面派生。
    • 派生自具體類,就會依賴具體類。
  • 子類不要覆蓋父類中已實現的方法。
    • 父類中已實現的方法應該被所有子類共享,而不是覆蓋。

當然事情沒有絕對的,上面三個指導方針,是應該儘量做到,而不是必須做到

4,抽象工廠模式

下面來介紹抽象工廠模式

假如臨近春節,飲料店老闆為了更好的銷售飲料,準備購買一批禮盒,來包裝飲料。為了保證禮盒的質量,規定禮盒只能從特定的地方批發,比如北京,上海等。

那我們就定義一個介面,讓所有的禮盒都派生自這個介面,如下:

interface DrinkBoxFactory {
    String createBox();
}

class BeiJingBoxFactory implements DrinkBoxFactory {

    @Override
    public String createBox() {
        return "BeijingBox";
    }
}

class ShangHaiBoxFactory implements DrinkBoxFactory {

    @Override
    public String createBox() {
        return "ShangHaiBox";
    }
}

下面需要編寫 Drink 類,我們讓 Drink 類是一個抽象類,如下:

abstract class Drink {
    String flavor;
    protected abstract void packing();
}

需要注意的是,在抽象類 Drink 中有一個抽象方法 packingDrinkpacking 的具體實現交給每個派生類。Drink 類只規定派生類中需要實現一個 packing,但並不關心它的具體實現。

下面編寫每種口味的飲料,如下:

class DrinkApple extends Drink {
    DrinkBoxFactory boxFactory;

    public DrinkApple(DrinkBoxFactory boxFactory) {
        this.boxFactory = boxFactory;
        this.flavor = "DrinkApple";
    }

    @Override
    public void packing() {
        System.out.println(flavor + boxFactory.createBox());
    }
}

class DrinkBanana extends Drink {
    DrinkBoxFactory boxFactory;

    public DrinkBanana(DrinkBoxFactory boxFactory) {
        this.boxFactory = boxFactory;
        this.flavor = "DrinkBanana";
    }

    @Override
    public void packing() {
        System.out.println(flavor + boxFactory.createBox());
    }
}

class DrinkOrange extends Drink {
    DrinkBoxFactory boxFactory;

    public DrinkOrange(DrinkBoxFactory boxFactory) {
        this.boxFactory = boxFactory;
        this.flavor = "DrinkOrange";
    }

    @Override
    public void packing() {
        System.out.println(flavor + boxFactory.createBox());
    }
}

可以看到每種口味的飲料中都有一個 boxFactory 物件,在 packing 時,從 boxFactory 中獲取禮盒。

注意 boxFactory 是一個介面類物件,而不是一個具體類物件,因此,boxFactory 的具體物件是由客戶決定的。

然後,DrinkStoreAbstract 還是沿用工廠方法模式中的定義,如下:

abstract class DrinkStoreAbstract {

    // final 防止子類覆蓋
    public final Drink sellDrink(String flavor) {

        Drink drink = factoryMethod(flavor);    // 使用例項

        drink.packing();

        return drink;
    }

    // 子類必須實現
    protected abstract Drink factoryMethod(String flavor);
}

然後我們實現使用北京禮盒包裝的Store 和使用上海禮盒包裝的Store,如下:

class BeijingDrinkStore extends DrinkStoreAbstract {
    
    @Override
    protected Drink factoryMethod(String flavor) {
        Drink drink = null;
        DrinkBoxFactory factory = new BeiJingBoxFactory();

        if (flavor.equals("apple")) {
            drink = new DrinkApple(factory);
        } else if (flavor.equals("banana")) {
            drink = new DrinkBanana(factory);
        } else if (flavor.equals("orange")) {
            drink = new DrinkOrange(factory);
        } 
                
        return drink;
    }
}

class ShangHaiDrinkStore extends DrinkStoreAbstract {

    @Override
    protected Drink factoryMethod(String flavor) {
        Drink drink = null;
        DrinkBoxFactory factory = new ShangHaiBoxFactory();

        if (flavor.equals("apple")) {
            drink = new DrinkApple(factory);
        } else if (flavor.equals("banana")) {
            drink = new DrinkBanana(factory);
        } else if (flavor.equals("orange")) {
            drink = new DrinkOrange(factory);
        }

        return drink;
    }
}

經過這麼一些列的改動,我們到底做了些什麼呢?事情的起因是老闆需要一些禮盒來包裝飲料,這就是需求增加了。

因此我們引入了一個抽象工廠,即 DrinkBoxFactory ,這個介面是所有禮盒工廠的父類,它給了所有子類一個“規範”。

抽象工廠模式提供了一個介面,用於建立相關物件的家族。該模式旨在為客戶提供一個抽象介面(本例中就是 DrinkBoxFactory 介面),從而去建立一些列相關的物件,而不需關心實際生產的具體產品是什麼,這樣做的好處是讓客戶從具體的產品中解耦

最終,客戶是這樣使用 DrinkStoreAbstract 的,如下:

public class AbstractMethod {
    public static void main(String[] args) {
        DrinkStoreAbstract beiStore = new BeijingDrinkStore();
        beiStore.sellDrink("apple");
        beiStore.sellDrink("banana");
        beiStore.sellDrink("orange");

        DrinkStoreAbstract shangStore = new ShangHaiDrinkStore();
        shangStore.sellDrink("apple");
        shangStore.sellDrink("banana");
        shangStore.sellDrink("orange");
    }
}

完整的抽象工廠模式程式碼放在了這裡,供大家參考,類圖如下:

在這裡插入圖片描述

從該圖可以看出,抽象工廠模式比工廠方法模式更復雜了一些,另外仔細觀察它們兩個的類圖,各自所關注的地方(粉紅色區域)也是不一樣的

工廠方法與抽象工廠的相同點都是將物件的建立封裝起來。不同點是:

  • 工廠方法主要關注將類的例項化推遲到子類中
  • 抽象工廠主要關注建立一系列相關的產品家族

5,總結

工廠模式使用到的設計原則有:

  • 針對介面程式設計,而非針對實現程式設計。
  • 開閉原則。
  • 依賴倒置原則。
  • 封裝變化。

本篇文章介紹了三種工廠模式,工廠模式在實際應用中非常廣泛,比如 Java 工具類中的 CalendarDateFormat 等。

(本節完。)


推薦閱讀:

單例模式-讓一個類只有一個例項

設計模式之高質量程式碼


歡迎關注作者公眾號,獲取更多技術乾貨。

碼農充電站pro

相關文章