跳至主要内容

Factory Method Pattern

工廠模式是一種創建型設計模式,在父類提供一個創建對象的方法,允許子類決定實例化對象的類型

工廠模式結構

工廠模式結構

工廠模式的應用場景

  1. 在寫程式時,如果無法預知對象確切類別以及他的依賴關係時

    工廠方法會將創建產品的程式碼和實際使用產品的程式碼分離,能在不影響其他程式碼的情況下擴展產品創建部分程式碼

  2. 如果希望用戶能夠擴展你的 library 或 framework 的內部組件

    將框架中構造組件的程式碼集中到一個工廠方法中,並在繼承組件之外允許任何人對該方法進行重寫

  3. 如果希望復用現有對象來節省系統資源,而不是每次都重新創建對象
    1. 首先,需要創建儲存空間來存放所有已創建的對象
    2. 當其他人請求一個對象時,程式在對象池中搜尋可以用的對象
    3. 然後將他返回給客戶端程式
    4. 如果沒有可用的對象,程式則創建一個新對象(將他添加到對向池中)

      因此,需要有一個既能夠創建新對象,又可以復用現有對象的方法,和工廠方法非常相像

優缺點

優點

  1. 可以避免創建者和具體產品之間的緊密耦合
  2. 單一職責原則。可以將產品創建程式碼放在程式的單一個位置,使得程式碼更容易維護
  3. 開放封閉原則。無須更改現有客戶程式碼,就可以在程式中引入新的產品類型

缺點

  1. 工廠模式需要引入許多新的子類,可能讓程式碼變得複雜。最好的情況是將該模式引入創建者類的現有層次結構中(產品類太多時,為每個產品創建子類並無太大必要,可以在子類中復用基類中的控制參數)

簡單工廠(Simple Factory)

原始的模型架構

  1. 首先你有一個 pizza,都會經過 prepare、bake、cut、box
public class PizzaStore {

public Pizza orderPizza() {
Pizza pizza = new Pizza(); // 為了更有彈性,我們想把它寫成抽象類別或介面,遺憾的是,抽象類別或介面都無法直接實例化。

pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

return pizza;
}
}
  1. 增加更多種類的 pizza,透過傳入參數決定
public Pizza orderPizza(String type) {
Pizza pizza;

/* 根據pizza的種類來實例化正確的具體類別,並指派給pizza實例變數 */
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("greek")) {
pizza = new GreekPizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
}

/* 做一些準備工作(揉麵團、放上醬料和佐料),烘烤、切開、放到盒子 */
/* 每一種pizza的子型態(CheesePizza、GreekPizza...)都知道要怎麼準備他 */
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

return pizza;
}

遇到的需求與問題

  1. 需要增加 ClamPizza 和 VeggiePizza,並刪除 GreekPizza
public Pizza orderPizza(String type) {
Pizza pizza;

/* 這是會變得部分,會隨著pizza品項不斷改變而反覆修改這段程式 */
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("greek")) { // 要刪除的greekpizza
pizza = new GreekPizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
} else if (type.equals("clam")) { // 新增clampizza
pizza = new ClamPizza();
} else if (type.equals("veggie")) { // 新增veggiepizza
pizza = new VeggiePizza();
}

/* 這是我們認為不變的部分,因為多年來,準備、烘烤、包裝都沒有變過。 */
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

return pizza;
}
備註

上面中間的程式並未拒絕修改。每次 pizza 改變品項,我們就必須修改這段程式碼。

建構簡單工廠(封裝物件的建立)

  1. 將會變得部分移出 oderpizza(),移到另一個單純負責建立 pizza 的物件裡面(SimplePizzaFactory)
public class SimplePizzaFactory {

public Pizza createPizza(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();
}
return pizza;
}
}

// ----------------------------------------------------------------

public class PizzaStore {
SimplePizzaFactory factory;

public PizzaStore(SimplePizzaFactory factory) { // 在建構是裡接收傳來的工廠
this.factory = factory;
}

public Pizza orderPizza(String type) {
Pizza pizza;

pizza = factory.createPizza(type); // 將訂單的種類傳給工廠,用他來建立pizza,這裡也沒有具體實例化了!

pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

return pizza;
}

}

沒有蠢問題

Q: 這樣做有什麼好處?只是把問題搬到另一個物件裡面

A: SimplePizzaFactory 可能有許多用戶端,未來可能還有 PizzaShopMenu 類別會使用這個工廠來取得 pizza,用來顯示 pizza 的說明和價格。也可能還有 HomeDelivery 類別會使用這個工廠來取得 pizza,採取和 PizzaStore 不一樣的方式來處理 pizza。所以將建立 pizza 的程式封裝成一個類別,在處理變動時,只需要修改一個地方。

Q: 有些會將工廠定義成靜態(static)方法,有什麼不同?

A: 將簡單工廠定義成靜態方法很常見,稱作靜態工廠,如此一來就不需要為了使用 create(建立)方法而進行工廠物件實例化,但這個做法有一項缺點,你無法繼承並修改 create 方法的行為。

定義簡單工廠

簡單工廠不是設計模式,比較像習慣寫法

簡單工廠例子

備註

實作介面 不一定代表 在類別宣告式裡面使用 implements 來編寫一個實作了 Java 介面的類別 。廣義來說,讓一個具體的類別實作超型態(可能是抽象類別或是介面)的方法,仍然可以視為 實作 那個超型態的 介面

工廠方法(Factory Method)

  1. 希望加盟 Pizza 店,因此創建兩個 NYPizzaFactory 和 ChicagoPizzaFactory,製作不同風味的 pizza。
NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");

// ----------------------------------------------------

ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.orderPizza("Veggie");
警告

加盟的 nyStore 和 chicagoStore 開始採取自創的流程,因此我們需要把變得部分移出,利用抽象物件保留不變的部分(Pizza 店和 Pizza 的製作過程綁再一起),同時保持一些彈性由子類自己決定

為 pizza 店設計框架、讓子類做決定

  1. 在 PizzaStore 類別裡,將所有的 pizza 製作動作局部化(localize),同時讓連鎖店可以自由地製作地區風味
public abstract class PizzaStore { // 宣告成抽象類別,讓子類實作工廠方法

public Pizza orderPizza(String type) {// 想要的話,可以將其設定為final,強迫子類一定要使用他
Pizza pizza;

pizza = createPizza(type); // 透過抽象方法,取得pizza,不再是factory物件的方法

/* 保留不變的地方,讓子類都能保持一致 */
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}

protected abstract Pizza createPizza(String item); // 現在「工廠方法」是在PizzaStore裡面的抽象方法,也強迫所有子類必須實現這個方法
}
  1. 創建兩間 Pizza 店,NYPizzaStore 和 ChicagoStylePizzaStore,他們都繼承 PizzaStore,子類別會用自己的 createPizza 方法製作地區風味 pizza
public class NYPizzaStore extends PizzaStore {// 繼承PizzaStore,所以繼承orderPizza()和其他方法

/* 必須實作createPizza,因為他是抽象的,並且會回傳一個Pizza,子類別全權負責決定他要實例化哪一種具體的Pizza */
Pizza createPizza(String item) { // 為每一種pizza製作紐約風味
if (item.equals("cheese")) {
return new NYStyleCheesePizza();
} else if (item.equals("veggie")) {
return new NYStyleVeggiePizza();
} else if (item.equals("clam")) {
return new NYStyleClamPizza();
} else if (item.equals("pepperoni")) {
return new NYStylePepperoniPizza();
} else return null;
}
}

// ----------------------------------------------------------------

public class ChicagoPizzaStore extends PizzaStore {

Pizza createPizza(String item) { // 為每一種pizza製作芝加哥風味
if (item.equals("cheese")) {
return new ChicagoStyleCheesePizza();
} else if (item.equals("veggie")) {
return new ChicagoStyleVeggiePizza();
} else if (item.equals("clam")) {
return new ChicagoStyleClamPizza();
} else if (item.equals("pepperoni")) {
return new ChicagoStylePepperoniPizza();
} else return null;
}
}

Q: 子類如何做決定?

A: 從 PizzaStore 的 orderPizza 想,在方法裡面用 Pizza 物件做了很多事,但 Pizza 是抽象的,具體型態只會在子類別裡面建立,換句話說,他們是解耦的。所以要看你向哪一個 pizza 店訂購 Pizza,是 NYPizzaStore 或 ChicagoPizzaStore。所以子類別做出及時的決定嗎?沒有,但從 orderPizza 的角度,決定做哪一種 Pizza 的就是那個子類別。所以子類別其實沒有「做決定」,決定向哪一間訂餐的人是你,但是他們確實決定了做出來的是哪種 Pizza。

  1. 製作 Pizza 介面(產品)和各式風味的 Pizza(具體產品)
public abstract class Pizza {
String name; // 名稱
String dough; // 餅皮
String sauce; // 醬料
ArrayList<String> toppings = new ArrayList<String>(); // 一組配料

void prepare() { // 準備流程是一組特定順序的步驟
System.out.println("Prepare " + name);
System.out.println("Tossing dough...");
System.out.println("Adding sauce...");
System.out.println("Adding toppings: ");
for (String topping : toppings) {
System.out.println(" " + topping);
}
}

/* 烘烤、切開、包裝的基本預設值 */
void bake() {
System.out.println("Bake for 25 minutes at 350");
}

void cut() {
System.out.println("Cut the pizza into diagonal slices");
}

void box() {
System.out.println("Place pizza in official PizzaStore box");
}

public String getName() {
return name;
}

public String toString() {
StringBuffer display = new StringBuffer();
display.append("---- " + name + " ----\n");
display.append(dough + "\n");
display.append(sauce + "\n");
for (String topping : toppings) {
display.append(topping + "\n");
}
return display.toString();
}
}

// ----------------------------------------------------------------

public class NYStyleCheesePizza extends Pizza {

public NYStyleCheesePizza() {
name = "NY Style Sauce and Cheese Pizza";
dough = "Thin Crust Dough"; // 薄餅皮
sauce = "Marinara Sauce"; // 紅醬

toppings.add("Grated Reggiano Cheese"); // 瑞吉起司的配料
}
}

// ----------------------------------------------------------------

public class ChicagoStyleCheesePizza extends Pizza {

public ChicagoStyleCheesePizza() {
name = "Chicago Style Deep Dish Cheese Pizza";
dough = "Extra Thick Crust Dough"; // 超厚餅皮
sauce = "Plum Tomato Sauce"; // 番茄醬

toppings.add("Shredded Mozzarella Cheese"); // 莫札瑞拉起司的配料
}

void cut() { // 芝加哥風味的Pizza覆寫cut方法,將Pizza切成方塊
System.out.println("Cutting the pizza into square slices");
}
}
  1. 撰寫客戶端測試訂購 Pizza
public class PizzaTestDrive {

public static void main(String[] args) {
// 建立兩個不同的Pizza店
PizzaStore nyStore = new NYPizzaStore();
PizzaStore chicagoStore = new ChicagoPizzaStore();

// 用一家店來製作Ethan的訂單
Pizza pizza = nyStore.orderPizza("cheese");
System.out.println("Ethan ordered a " + pizza.getName() + "\n");

// 用另一家店來製作Joel的訂單
pizza = chicagoStore.orderPizza("cheese");
System.out.println("Joel ordered a " + pizza.getName() + "\n");
}
}

建立者和產品視為平行的

定義工廠模式

提示

工廠方法模式定義了一個創建物件的介面,但是他讓子類別決定想要實例化哪一個類別。工廠方法可以讓一個類別將實例化的動作推遲到子類別。

工廠方法模式可以讓我們將具體型態的實例化封裝起來。所以通常會聽到開發者說:「工廠方法模式可以讓子類別決定想要實例化的類別。」

沒有蠢問題

Q: 如果 ConcreteCreator 只有一個時,工廠方法有什麼好處?

A: 只有一個還是可以防止產品的實作和他的用法出現耦合,另外需要加入額外的產品或改變產品的實作,並不會影響你的 Creator。

Q: 我們可以說 NYStylePizzaStore 和 ChicagoStylePizzaStore 都是用簡單工廠(Simple Factory)實作的嗎?

A: 兩種做法很像,但用法不同。具體的 Pizza 店看起來很像 SimplePizzaFactory,但具體 Pizza 店繼承的類別定義的一個抽象方法 createPizza(),而 createPizza()方法是由每一家店定義的。在簡單工廠裡,工廠是與 PizzaStore 組合在一起的另一個物件。

Q: 工廠方法和 Creator 類別一定要是抽象的嗎?

A: 不是,可以定義預設的工廠方法來產生具體的產品。即使 Creator 沒有任何子類,還是可以製作產品。

Q: 每一個具體建立者都要製作多個產品嗎?還是可以只製作一個?

A: 我們製作的結構就是參數化工廠方法,可以根據傳來的參數製作多個物件。但是,工廠通常只會製作一個物件,而不使用參數。兩者型式都是有用的模式。

Q: 用 String 型態來傳遞參數似乎不太安全,如果有人把 ClamPizza 寫成 CalmPizza?

A: 對的,這會造成執行期錯誤,可以使用代表參數種類的物件、使用靜態常數、或使用 enum,確保編譯期可以抓到。

Q: 簡單工廠和工廠方法還有沒有差異?

A: 簡單工廠可以將物件的建立封裝起來,但是無法提供工廠方法的彈性,因為你無法改變你所建立的產品。

依賴反轉原則(Dependency Inversion Principle)

資訊

要依賴抽象,不要依賴具體類別

與「針對介面寫程式,不要針對實作寫程式」很像,但依賴反轉更強調抽象,提出高階的組件不應該依賴低階的組件,兩者都要依賴抽象。

第一版的 PizzaStore

  • 因為 PizzaStore 直接建立這些 Pizza 物件,所以他依賴這些物件。如果這些 Pizza 的具體實作改變了,有可能就要修改 PizzaStore,所以我們說 PizzaStore依賴Pizza 具體實作。

第一版PizzaStore依賴圖

使用工廠模式後的 PizzaStore

  • 主要問題是 PizzaStore 他依賴每一種 Pizza,因為 orderPizza 裡實例化具體型態。雖然建立一個抽象 Pizza,但我們仍建立許多具體的 Pizza。使用工廠模式,將 orderPizza 方法裡的實例化拿出來後,就可以解決依賴。此時 PizzaStore 只依賴 Pizza 介面,他是抽象類別,Pizza 介面也是抽象的,具體 Pizza 實作 Pizza 介面,所以依賴他。

工廠模式後的PizzaStore依賴圖

以下方針可以避免設計違反依賴反轉原則

  1. 任何變數都不應該保存具體類別的參考

    當你使用 new,你就會保存一個指向具體類別的參考,使用工廠來避免

  2. 任何類別都不應該從具體類別衍生出來

    從具體類別衍生,就依賴一個具體類別

  3. 任何方法都不應該覆寫基底類別的任何已實作的方法

    覆寫已實作的方法,就代表你的基底類別從一開始就不是真正的抽象。基底類的實作方法,是為了讓所有子類別共用的

上面的原則是你要盡量遵守,不是不留餘地的死規則,目的是在違反原則時能立刻察覺,並知道何時有充分的理由可以違反原則

  • 我們經常不假思索的實例化 String 物件,有違反原則嗎?有,可以這樣做嗎?可以,為什麼?因為 String 非常不可能改變

工廠方法的例子

JDK 的 java.util.Collection#iterator

  • Collection 介面就是一個抽象工廠,ArrayList 具體工廠
  • Iterator 介面是抽象產品,ListIterator 是抽象產品

JDK中的應用

Lambda 和 Enum 的寫法

只有 Lambda

優點

  1. 可以減少 Class 的數量

缺點

  1. 工廠需要多個引數時無法使用
// ---------------------將工廠類改成Lambda-------------------
public class NYPizzaStore extends PizzaStore {

// 改成Map對應
private static final Map<String, Optional<Supplier<Pizza>>> map = new ConcurrentHashMap<>();

static {
map.put("NYStyleCheesePizza", Optional.of(NYStyleCheesePizza::new));
map.put("NYStyleVeggiePizza", Optional.of(NYStyleVeggiePizza::new));
map.put("NYStyleClamPizza", Optional.of(NYStyleClamPizza::new));
map.put("NYStylePepperoniPizza", Optional.of(NYStylePepperoniPizza::new));
}

static Pizza createPizza(String item) { // 選擇哪一種紐約風味的Pizza
return map.get(item)
.orElseThrow(() -> new IllegalArgumentException("No such item: " + item))
.get();
}
}

Lambda + Enum

優點

  1. Enum 可以不用寫 Optional

缺點

  1. 如果使用 Enum 則會造成選擇的邏輯無法集中在工廠裡面
// -----------------------------建立ENUM類-----------------------------
public enum NYStylePizzaEnum {
NYStyleCheesePizza(NYStyleCheesePizza::new),
NYStyleVeggiePizza(NYStyleVeggiePizza::new),
NYStyleClamPizza(NYStyleClamPizza::new),
NYStylePepperoniPizza(NYStylePepperoniPizza::new);

private final Supplier<Pizza> constructor;

NYStylePizzaEnum(Supplier<Pizza> constructor) {
this.constructor = constructor;
}

public Supplier<Pizza> getConstructor() {
return constructor
}
}

// -----------------------------將工廠改為使用ENUM類----------------------

public class NYPizzaStore extends PizzaStore {

// 使用Enum來選擇,但如何判斷使用哪一個Enum的邏輯可能無法集中,會跑到工廠外部
Pizza createPizza(NYStylePizzaEnum NYStylePizza) {
return NYStylePizza.getConstructor().get();
}
}