深入淺出設計模式(Design Pattern)-工廠模式(4-Factory-Pattern)

Ben
生活的各種可能性
26 min readJan 29, 2024
photo by carlos aranda

看到這張照片是不是就知道這篇要來介紹什麼了,沒錯,就是工廠模式(Factory Pattern)。

工廠模式(Factory Pattern)

在這篇的工廠模式中,我們將會分成兩個部分來介紹,分別是工廠方法模式(Factory Method Pattern)抽象工廠模式(Abstract Factory Pattern)。

情境:我們今天有一間披薩店,然後我們有非常多的披薩種類,而我們一開始是想法是這樣,以下是個概念,並不能實際運作。

我們有個製作披薩的方法,它會實例化一個披薩,並有許多完成披薩的流程方法,最後會回傳披薩。

orderPizza() {
pizza = new Pizza();

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

return pizza;
}

但我們為了考量更多的彈性,改寫成這樣

orderPizza(type) {
let pizza;

if (type === "cheese") {
pizza = new CheesePizza();
} else if (type === "greek") {
pizza = new GreekPizza();
} esle if (type === "pepperoni") {
pizza = new PepperoniPizza();
}

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

return pizza;
}

但是披薩的種類一直因應市場的需求而有所變化,而我們就必須一直更改裡面的內容(增加或移除披薩種類)。但我們現在知道會變的地方了,那我們就應該把這個部分封裝起來,於是我們創造出了下方的簡單工廠。

在下方的例子中我們將Pizz店和製作披薩的內容解耦合了,具體Pizza的實作是由工廠去決定的,商店只負責接受訂單和其他製作披薩的SOP。

class SimplePizzaFactory {
public createPizza(type: string) {
pizza: null = null;

if (type === "cheese") {
pizza = new CheesePizza();
} else if (type === "greek") {
pizza = new GreekPizza();
} esle if (type === "pepperoni") {
pizza = new PepperoniPizza();
}
}
}

class PizzaStore {
factory: SimplePizzaFactory;

constructor(factory: SimplePizzaFactory) {
this.factory = factory;
}

public orderPizza(type: string) {
let pizza: null = null;

pizza = this.factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

return pizza;
}

// 其他的方法...
}

而現在我們有一個新的問題,我們想要將披薩店開放加盟,而加盟店也將因為它的地區而有不同的口味,並且他們也會有一些自己獨特的製作方式,這時候你意識到,必須將披薩店和披薩的製作綁在一起,同時也要保持一些彈性,但在一開始時我們是這麼做的,但它並不靈活,那我們到底該怎麼做呢?

工廠方法模式(Factory Method Pattern)

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

我們來看一下它是如何運作的

abstract class PizzaStore {
public orderPizza(type: string) {
let pizza: Pizza | null;

// 在點餐時我們會呼叫createPizza方法來幫我們取的我們要的Pizza,
// 而order披薩並不需要知道實際的產品(Pizza)會是什麼
pizza = createPizza(type);

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

return pizza;
}

// 我們將createPizza宣告成抽象的,由子類別實現(實例化Pizza的職責都被移到扮演工廠的方法裡了)
protected abstract createPizza(type: string);

// 其他的方法
}

工廠方法模式可讓我們將具體型態的實例化封裝起來,Creator(PizzaStore)提供一個介面,裡面有一個建立物件的方法(createPizza),也稱為工廠方法。而依據具體的情況來使用某個子類別來建立產品。

再來別忘了我們的披薩

abstract class Pizza {
name: string = "";
dough: string = "";
sauce: string = "";
toppings: string[] = [];

public prepare() {
// do something...
}

// 具體製作流程
public bake() {}
public cut() {}
public box() {}
public setName(name: string) {}
public getName() {}
}

了解物件的依賴關係

我們來看一下我們一開始的例子間的依賴關係,我們是在PizzaStore類別裡建立所有的Pizza物件,而不是將這件事情委託給工廠。

而這樣則會在將來的變化中衍生許多的問題。

依賴關係示意圖1

在程式中,降低對於具體類別的依賴程度是一件好事情。

而有一條物件導向設計原則指出這一點,也就是依賴反轉原則(Dependency Inversion Principle)。而通則如下

設計原則:要依賴抽象,不要依賴具體類別。

依賴反轉原則

這條原則跟針對介面寫程式,不要針對實作寫程式很像,但是依賴反轉原則更強調抽象。它提出高階的組件不應該依賴低階的組件,而是兩者都要依賴抽象。而在運用工廠方法模式後我們的依賴關係圖則變成如下

依賴關係示意圖2

在採用工廠方法之後,我們可以看到,高階組件(PizzaStore)與低階組件(Pizza)都依賴Pizza抽象。而你可能會有個問題,依賴原則到底反轉了什麼?

我們看回到依賴關係示意圖1,低階組件依賴一個高階的抽象,高階組件也連到同一個抽象,而在依賴關係示意圖2中從上到下(一般人中所認為的物件導向設計是上到下)的依賴關係已經反轉過來了,高階與低階組件都依賴抽象。

避免你的設計違反依賴反轉原則

  • 任何變數都不應該保存具體類別的參考:當你使用new時,你就會保存一個指向具體類別的參考。
  • 任何類別都不應該從具體類別衍生出來:如果從具體類別衍生,你就依賴一個具體的類別。
  • 任何方法都不應該覆寫基底類別的任何已實作的方法:如果覆寫已實作的方法,代表你的基底類別一開始就不是真正的抽象。在基底類別裡實作的方法是為了讓所有子類別共用的。

而現在的我們又碰到新的問題,各個地區的披薩店都用同樣的材料但卻有屬於該地區的特色,例如紐約的紅醬跟芝加哥的紅醬不同等等,所以我們必須準備兩種紅醬分別送到紐約及芝加哥。

抽象工廠模式(Abstract Factory Pattern)

定義:提供一個介面來建立或相依的物件家族,而不需要指定具體類別。

接著我們來看它是如何運作的。

首先我們會替食材工廠定義一個完整的介面

// 為每一種食材定義一個create方法
interface PizzaIngredientFactory {
createDough(): string;
createSauce(): string;
createCheese(): string;
createVeggies(): string;
createPepperoni(): string;
createClam(): string;
}

接著我們會實作一個紐約風格披薩的食材工廠

// 這個工廠負責製作紐約風格披薩的食材
class NYPizzaIngredientFactory implements PizzaIngredientFactory {
public createDough() {
// do something...
}

public createSauce() {
// do something...
}

public createCheese() {
// do something...
}

public createVeggies() {
// do something...
}

public createPepperoni() {
// do something...
}

public createClam() {
// do something...
}
}

再來我們必須修改一下抽象類別Pizza

abstract class Pizza {
// 每一個披薩都會擁有自己一組食材
name: string = "";
dough: string = "";
veggies: string[] = [];
cheese: string = "";
pepperoni: string = "";
clams: string = "";

// 我們將prepare改成抽象的,將它的實作交給子類別實現
abstract prepare(): void;

// 具體製作流程
public bake() {}
public cut() {}
public box() {}
public setName(name: string) {}
public getName() {}
}

實體化我們的Cheese披薩

class CheesePizza extends Pizza {
// 每一個披薩都有一個食材工廠變數
ingredientFactory: PizzaIngredientFactory;

// 建構式接受一個食材工廠並初始化
constructor(ingredientFactory: PizzaIngredientFactory) {
this.ingredientFactory = ingredientFactory;
}

// 我們實作抽象方法prepare,它將調用在建構式中接受的工廠來產生相應的食材
prepare(): void {
this.dough = this.ingredientFactory.createDough();
this.sauce = this.ingredientFactory.createSauce();
this.cheese = this.ingredientFactory.createCheese();
}
}

在Pizza中,Pizza並不關心食材是什麼(它只在乎它是不是實作了工廠介面),食材是由工廠來決定的,Pizza只知道如何製作披薩,這樣的做法讓我們將Pizza和食材解耦合,我們可以輕鬆地重複使用Pizza。

我們即將完成我們的披薩店

class NYPizzaStore extends PizzaStore {
createPizza(item: string) {
let pizza = null;
// 紐約店與紐約的食材工廠會用來生產紐約風格的披薩
let ingredientFactory = new NYPizzaIngredientFactory();

// 我們將食材工廠傳給每一個不同風格的披薩
if (item === "cheese") {
pizza = new CheesePizza(ingredientFactory);
pizza.setName("New York Style Cheese Pizza");
} else if (item === "veggie") {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("New York Style Veggie Pizza");
} else if (item === "clam") {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("New York Style Clam Pizza");
} else if (item === "pepperoni") {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("New York Style Pepperoni Pizza")
}

return pizza;
}
}

我們實作了一個紐約風格的披薩店,它有一個自己專屬的紐約風格食材工廠。

我們應用了抽象工廠方法來打造我們的食材工廠。抽象工廠提供了一種介面來讓我們建立一個產品家族。使用這樣的方式讓程式碼與建立產品的實際工廠解耦合。這樣就可以讓我們製作各種工廠並生產各種不同的產品(使用各種工廠來取得產品的各種實作),而用戶端保持不變。

而在我們上方定義的抽象工廠介面就是一整組的工廠方法,它們都分別負責建立一個具體的產品(食材)。

以下是整個完整的範例:

type PizzaType = "cheese" | "veggie" | "clam" | "pepperoni"

// PizzaStore(抽象類別 - 高階):不需要知道會提供什麼產品
abstract class PizzaStore<T extends Pizza<PizzaIngredientFactory>> {
public orderPizza(name: string, type: PizzaType) {
let pizza: Pizza<PizzaIngredientFactory> | null = this.createPizza(type);
console.log(`--------------- New Order from ${name} ---------------`);

if (pizza !== null) {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}

return pizza;
}

// 由子類別來實作實際產生披薩方法的內容
protected abstract createPizza(type: PizzaType): T | null;
}

// Pizza(抽象類別 - 低階):所有的披薩產品都必須實作的Pizza抽象類別
abstract class Pizza<T extends PizzaIngredientFactory> {
name: string = "";
dough: string = "";
sauce: string = "";
veggies: string[] = [];
cheese: string = "";
clam: string = "";
toppings: string[] = [];
ingredientFactory: T;

// Pizza的抽象類別建構式接收一個實體食材工廠(每個披薩都有自己的食材工廠)
constructor(ingredientFactory: T) {
this.ingredientFactory = ingredientFactory;
}

// 由子類別來實作準備披薩的實際細節
public abstract prepare(): void;

public bake(): void {
console.log("Bake for 25 minutes at 350");
};

public cut(): void {
console.log("Cutting the pizza into diagonal slices");
};

public box(): void {
console.log("Place pizza in official PizzaStore box");
};

public setName(name: string): void {
this.name = name;
};

public getName(): string {
return this.name;
};
}

// 共用的食材工廠介面
interface PizzaIngredientFactory {
createDough(): string;
createSauce(): string;
createCheese(): string;
createVeggies(): string;
createPepperoni(): string;
createClam(): string;
}

// 實作紐約風的食材工廠
class NYPizzaIngredientFactory implements PizzaIngredientFactory {
createDough(): string {
console.log("add NY Style Dough");
return "NY Style Dough";
};
createSauce(): string {
console.log("add NY Style Sauce");
return "NY Style Sauce";
};
createCheese(): string {
console.log("add NY Style Cheese");
return "NY Style Cheese";
};
createVeggies(): string {
console.log("add NY Style Veggies");
return "NY Style Veggies";
};
createPepperoni(): string {
console.log("add Pepperoni");
return "Pepperoni";
};
createClam(): string {
console.log("add Clam");
return "Clam";
};
}

// 實作芝加哥風的食材工廠
class ChgPizzaIngredientFactory implements PizzaIngredientFactory {
createDough(): string {
console.log("add Chicago Style Dough");
return "Chicago Style Dough";
};
createSauce(): string {
console.log("add Chicago Style Sauce");
return "Chicago Style Sauce";
};
createCheese(): string {
console.log("add Chicago Style Cheese");
return "Chicago Style Cheese";
};
createVeggies(): string {
console.log("add Chicago Style Veggies");
return "Chicago Style Veggies";
};
createPepperoni(): string {
console.log("add Pepperoni");
return "Pepperoni";
};
createClam(): string {
console.log("add Clam");
return "Clam";
};
}

// 實作Chesse披薩
class CheesePizza<T extends PizzaIngredientFactory> extends Pizza<PizzaIngredientFactory> {
constructor(ingredientFactory: T) {
super(ingredientFactory);
}

// 披薩使用自己在建構式中接收的工廠,並使用工廠產生的食材去實作自己如何準備披薩的內容
prepare(): void {
console.log(`Preparing ${this.name}`);
this.dough = this.ingredientFactory.createDough();
this.sauce = this.ingredientFactory.createSauce();
this.cheese = this.ingredientFactory.createCheese();
}
}

// 實作Clam披薩
class ClamPizza<T extends PizzaIngredientFactory> extends Pizza<PizzaIngredientFactory> {
constructor(ingredientFactory: T) {
super(ingredientFactory);
}

// 披薩使用自己在建構式中接收的工廠,並使用工廠產生的食材去實作自己如何準備披薩的內容
prepare(): void {
console.log(`Preparing ${this.name}`);
this.dough = this.ingredientFactory.createDough();
this.sauce = this.ingredientFactory.createSauce();
this.cheese = this.ingredientFactory.createCheese();
this.clam = this.ingredientFactory.createClam();
}
}

// 實作Veggie披薩
class VeggiePizza<T extends PizzaIngredientFactory> extends Pizza<PizzaIngredientFactory> {
constructor(ingredientFactory: T) {
super(ingredientFactory);
}

// 披薩使用自己在建構式中接收的工廠,並使用工廠產生的食材去實作自己如何準備披薩的內容
prepare(): void {
console.log(`Preparing ${this.name}`);
this.dough = this.ingredientFactory.createDough();
this.sauce = this.ingredientFactory.createSauce();
this.cheese = this.ingredientFactory.createCheese();
this.clam = this.ingredientFactory.createClam();
}
}

// 實作pepperoni披薩
class PepperoniPizza<T extends PizzaIngredientFactory> extends Pizza<PizzaIngredientFactory> {
constructor(ingredientFactory: T) {
super(ingredientFactory);
}

// 披薩使用自己在建構式中接收的工廠,並使用工廠產生的食材去實作自己如何準備披薩的內容
prepare(): void {
console.log(`Preparing ${this.name}`);
this.dough = this.ingredientFactory.createDough();
this.sauce = this.ingredientFactory.createSauce();
this.cheese = this.ingredientFactory.createCheese();
this.clam = this.ingredientFactory.createClam();
}
}

// 實作紐約風格Pizza店
class NYPizzaStore extends PizzaStore<Pizza<PizzaIngredientFactory>> {
// 實作紐約風格披薩店裡提供哪些披薩
createPizza(item: PizzaType) {
let pizza: Pizza<PizzaIngredientFactory> | null = null;
let ingredientFactory: PizzaIngredientFactory = new NYPizzaIngredientFactory();

if (item === "cheese") {
pizza = new CheesePizza(ingredientFactory);
pizza.setName("New York Style Cheese Pizza");
} else if (item === "veggie") {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("New York Style Veggie Pizza");
} else if (item === "clam") {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("New York Style Clam Pizza");
} else if (item === "pepperoni") {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("New York Style Pepperoni Pizza");
}

return pizza;
}
}

// 實作芝加哥風格Pizza店
class ChgPizzaStore extends PizzaStore<Pizza<PizzaIngredientFactory>> {
// 實作芝加哥風格披薩店裡提供哪些披薩
createPizza(item: PizzaType) {
let pizza: Pizza<PizzaIngredientFactory> | null = null;
let ingredientFactory: PizzaIngredientFactory = new ChgPizzaIngredientFactory();

if (item === "cheese") {
pizza = new CheesePizza(ingredientFactory);
pizza.setName("Chicago Style Cheese Pizza");
} else if (item === "veggie") {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("Chicago Style Veggie Pizza");
} else if (item === "clam") {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("Chicago Style Clam Pizza");
} else if (item === "pepperoni") {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("Chicago Style Pepperoni Pizza");
}

return pizza;
}
}

// Pizza點餐測試
class PizzaOrderMachine {
pizza: Pizza<PizzaIngredientFactory> | null = null;
nyPizzaStore: PizzaStore<Pizza<PizzaIngredientFactory>> = new NYPizzaStore();
chgPizzaStore: PizzaStore<Pizza<PizzaIngredientFactory>> = new ChgPizzaStore();

public orderNewYorkStylePizza(name: string, type: PizzaType) {
this.pizza = this.nyPizzaStore.orderPizza(name, type);
if (this.pizza !== null) {
console.log(`${name} ordered a ${this.pizza.getName()}`);
}
}

public orderChicagoStylePizza(name: string, type: PizzaType) {
this.pizza = this.chgPizzaStore.orderPizza(name, type);
if (this.pizza !== null) {
console.log(`${name} ordered a ${this.pizza.getName()}`);
}
}
}

const pizzaOrderMachine = new PizzaOrderMachine();

pizzaOrderMachine.orderNewYorkStylePizza("Ellen", "cheese");
pizzaOrderMachine.orderChicagoStylePizza("Roy", "clam");

重點提示

接下來我們來看看這個章節中重要的重點提示:

  • 所有工廠都封裝物件的創建。
  • 工廠方法依靠繼承,它將物件的創建委託給子類別,由子類別實作工廠方法來建立物件。
  • 抽象工廠依靠物件組合,物件的創建是在工廠的介面公開的方法裡面。
  • 所有的工廠模式都藉著降低應用程式對具體類別的依賴程度來促進鬆耦合。
  • 工廠方法的目的是讓類別將實例化的動作推遲到它的子類別。
  • 抽象工廠的目的是建立一系列相關的物件,而不需要依靠它們的具體類別。
  • 依賴反轉原則指引我們避免依賴具體型態,而是盡量依賴抽象。
  • 工廠這種強大的技術可以讓我們針對抽象寫程式,而不是針對具體類別。

結語

之前一直不太能理解依賴反轉原則到底轉了什麼,以前可能是轉爆了我的腦袋還是沒轉過來,現在則是轉了我對它們依賴關係的認知。在工廠模式中將依賴抽象而不依賴實體、封裝、解耦合這些原則提供了一個很棒的範例。

下一篇要介紹:單例模式(Singleton Pattern)

這篇就到這邊瞜,如果有任何問題歡迎留言給我,喜歡的話也別忘了clap起來唷。

--

--