Factory Pattern 工廠模式 [Design Pattern in Java]

Posted by Kubeguts on 2020-07-26

工廠模式提供了可創建(new)各種不同類別(class)的實例(instance)

工廠模式主要又涵蓋了 (1) 簡單工廠模式 Simple Factory Pattern (2) 工廠方法模式 (3) 抽象方法模式

簡單工廠方法是最初階的用法,會違反OOP的Open-close principle(OCP)
然而工廠方法和抽象方法則比較能讓模組間耦合度降低。

以下會依序做介紹

簡單工廠模式 Simple Factory Method

簡單工廠模式僅提供一個工廠類別,接收參數(決定生產哪種產品),來產生出對應的產品物件(Object)

以生產Pizza為例,假設我們有個披薩簡單工廠(SimplePizzaFactory)可以生產兩種不同口味的Pizza(Cheese和Potato):

SimplePizzaFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
public class SimplePizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza = null;

if(type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("potato")) {
pizza = new PotatoPizza();
}
return pizza;
}
}

工廠會回傳抽象的Pizza類別

Pizza.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class Pizza {
String name;
String ingridient;

void prepare() {
System.out.println("Preparing" + name);
System.out.println("Ingredient" + ingridient);
}

void bake() {
System.out.println("Bake for 20 mins");
}

String getName() {
return name;
}
}

以及實作了兩個口味的披薩 CheesePizza和PotatoPizza

CheesePizza.java

1
2
3
4
5

public class CheesePizza extends Pizza {
String name = "CheesePizza";
String ingridient = "Cheese";
}

PotatoPizza.java

1
2
3
4
public class PotatoPizza extends Pizza {
String name = "PotatoPizza";
String ingridient = "Potato";
}

Pizza都定義完畢後,定義SimplePizzaFactory.java

SimplePizzaFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SimplePizzaFactory {
public void orderPizza(String type) {
Pizza pizza = null;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
}

public Pizza createPizza(String type) {
Pizza pizza = null;
if(type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("potato")) {
pizza = new PotatoPizza();
}
return pizza;
}
}

在Main.java透過SimplePizzaFactory製作兩個Pizza:

Main.java

1
2
3
4
5
6
7
8
9

public class Main {
public static void main(String[] args) {
SimplePizzaFactory simplePizzaFactory = new SimplePizzaFactory();

simplePizzaFactory.orderPizza("cheese");
simplePizzaFactory.orderPizza("potato");
}
}

執行Main.java

1
2
3
4
5
6
Preparing: CheesePizza
Ingredient: Cheese
Bake for 20 mins
Preparing: PotatoPizza
Ingredient: Potato
Bake for 20 mins

但此若之後要擴充Pizza的種類,例如要多一個叫做OnionPizza,勢必得修改SimplePizzaFacotory.java

SimplePizzaFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SimplePizzaFactory {
public void orderPizza(String type) {
Pizza pizza = null;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
}

public Pizza createPizza(String type) {
Pizza pizza = null;
if(type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("potato")) {
pizza = new PotatoPizza();
} else if (type.equals("onion")) { // 新增洋蔥口味pizza
pizza = new OnionPizza();
}
return pizza;
}
}

:::danger
上述做法破壞了OOD的OCP(Open-Close Principle)的原則,亦即進行擴充時不修改到SimplePizzaFactory的程式碼。
:::

工廠方法模式

為了改善簡單工廠模式,工廠方法模式透過"抽象方法"的模式,讓之後擴充的工廠實作該"抽象方法",以進行其他類別的擴充

舉例:有個PizzaFactory負責定義了Pizza工廠該做的一件事(抽象方法)
叫做 createPizza();

PizzaFactory.java

1
2
3
4
5
6
7
8
9
10
public abstract class PizzaFactory {
public void orderPizza(String type) {
Pizza pizza = null;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
}

abstract Pizza createPizza(String type);
}

原本我們已經有兩種口味(Cheese和Potato) Pizza,叫做原味披薩(OriginalPizzaFactory), 使OringinalPizzaFactory去實作PizzaFactory的createPizza()方法

OringinalPizzaFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
public class OriginalPizzaFactory extends PizzaFactory {

public Pizza createPizza(String type) {
Pizza pizza = null;
if(type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("potato")) {
pizza = new PotatoPizza();
}
return pizza;
}
}

若要新增洋蔥口味的Pizza的話,那就直接在定義一個額外的工廠叫做,特別披薩工廠 (SpecialPizzaFactory)進行擴充,如此一來就不用更動到到原本原味披薩工廠(OriginalPizzaFactory)的程式碼

SpecialPizzaFactory.java

1
2
3
4
5
6
7
8
9
public class SpecialPizzaFactory extends PizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza = null;
if(type.equals("onion")) {
pizza = new OnionPizza();
}
return pizza;
}
}

記得多新增一個洋蔥披薩的類別

OnionPizza.java

1
2
3
4
5
6
public class OnionPizza extends Pizza {
public OnionPizza() {
name = "OnionPizza";
ingridient = "Onion";
}
}

在Main.java中呼叫原味和特別口味的披薩

Main.java

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
PizzaFactory originalPizzaFactory = new OriginalPizzaFactory();

originalPizzaFactory.orderPizza("cheese");
originalPizzaFactory.orderPizza("potato");

PizzaFactory specialPizzaFactory = new SpecialPizzaFactory();
specialPizzaFactory.orderPizza("onion");
}
}

透過下面的圖可以看到,透過定義一個抽象方法的工廠,若要擴充產品(在這裡我們用的是Pizza)類別的話,透過實作該抽象方法工廠,來進行擴充。

由於我們亦把Pizza類別給獨立出來成為一個抽象類別,若要新增其他口味的Pizza也可以達到不違反OCP精神的擴充,如下圖所示:

抽象工廠

抽象工廠顧名思義就是定義一個抽象介面工廠,欲擴充的話就實作該抽象介面工廠

以披薩原料工廠(PizzaIngredientFactory)為例,有醬料和蔬菜兩種原料:
方面給之後我們要製作的中式和美式披薩的原料實作,做擴展:

PizzaIngredientFactory.java

1
2
3
4
public interface PizzaIngredientFactory {
public Sauce createSauce();
public Vegetable createVegetable();
}

抽象工廠(原料工廠)在透過工廠方法來製作原料產品

我們再分別產生美式版本(AmericaIngredientFactory)和中式版本(ChinesePizzaFactory)的披薩原料工廠,可以看到使用了createSauce和createVegetable,透過工廠方法來產生原料產品

AmericaIngredientFactory.java

1
2
3
4
5
6
7
8
public class AmericaIngredientFactory implements PizzaIngredientFactory {
public Sauce createSauce() {
return new TomatoSauce();
}
public Vegetable createVegetable() {
return new Potato();
}
}

ChineseIngredientFactory.java

1
2
3
4
5
6
7
8
public class ChineseIngredientFactory implements PizzaIngredientFactory {
public Sauce createSauce() {
return new SoySauce();
}
public Vegetable createVegetable() {
return new Onion();
}
}

接著定義剛剛原料工廠有使用到的原料類別們(Sauce: SoySauce與PotatoSauce; Vegetable: Onion和Potato,該原料亦在實作Pizza類別的具體類別亦會用到

:::info
Pizza.java

1
2
3
4
5
6
7
8
9
public abstract class Pizza {
String name;

// 在Pizza類別中已經有宣告會用到的原料有哪些,是以介面來實作,方便做擴展
Sauce sauce;
Vegetable vegetable;
...

}

:::

定義醬料Sauce原料們:

Sauce.java

1
2
3
4
5
public abstract class Sauce {
String name;

public abstract String getSauce();
}

SoySauce.java

1
2
3
4
5
6
7
8
9
10
public class SoySauce extends Sauce {
public SoySauce() {
name = "soy sauce";
}

public String getSauce() {
System.out.println("The sauce is :" + name);
return this.name;
}
}

TomatoSauce.java

1
2
3
4
5
6
7
8
9
10
11

public class TomatoSauce extends Sauce {
public TomatoSauce() {
name = "tomato sauce";
}

public String getSauce() {
System.out.println("The sauce is :" + name);
return this.name;
}
}

接下來換定義Vegetable原料們

Vegetable.java

1
2
3
4
5
public abstract class Vegetable {
String name;

public abstract String getVegetable();
}

Onion.java

1
2
3
4
5
6
7
8
9
public class Onion extends Vegetable {
public Onion() {
name = "onion";
}
public String getVegetable() {
System.out.println("The vegetable is :" + name);
return this.name;
}
}

Potato.java

1
2
3
4
5
6
7
8
9
10
11

public class Potato extends Vegetable {
public Potato() {
name = "potato";
}

public String getVegetable() {
System.out.println("The vegetable is :" + name);
return this.name;
}
}

將披薩修改成包含上述原料 (Sauce和Vegetable)

Pizza.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class Pizza {
String name;

Sauce sauce;
Vegetable vegetable;

// 將prepare修改成抽象,該方法需要收集Pizza所需的原料,原料來自於原料工廠!
public abstract void prepare();

public void bake() {
System.out.println("Bake for 30 mins");
}

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

public String getName() {
return name;
}
}

定義兩個style的Pizza: 中式披薩(Chinese Pizza)和美式披薩(America Pizza),在這兩隻類別可以看到我們傳入了原料工廠,以決定哪種風格的披薩就用對應風格的原料工廠

ChinesePizza.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ChinesePizza extends Pizza {
PizzaIngredientFactory ingredientFactory;

public ChinesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}

public void prepare() {
System.out.println("Preparing: " + name);

sauce = ingredientFactory.createSauce();
sauce.getSauce();

vegetable = ingredientFactory.createVegetable();
vegetable.getVegetable();
}
}

AmericaPizza.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class AmericaPizza extends Pizza {
PizzaIngredientFactory ingredientFactory;

public AmericaPizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}

public void prepare() {
System.out.println("Preparing: " + name);

sauce = ingredientFactory.createSauce();
sauce.getSauce();

vegetable = ingredientFactory.createVegetable();
vegetable.getVegetable();
}
}

接著再利用工廠方法,定義中式和美式的披薩工廠,製作出中式和美式的披薩

PizzaFactory.java

1
2
3
4
5
6
7
8
9
10
public abstract class PizzaFactory {
public void orderPizza(String type) {
Pizza pizza = null;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
}

abstract Pizza createPizza(String type);
}

AmericaPizzaFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AmericaPizzaFactory extends PizzaFactory {
protected Pizza createPizza(String style) {
Pizza pizza = null;

// 使用了美式原料工廠
PizzaIngredientFactory ingredientFactory = new AmericaIngredientFactory();

if(style.equals("America")) {
pizza = new AmericaPizza(ingredientFactory);
pizza.setName("America style pizza");
}

return pizza;
}
}

:::info
可以在對美式工廠新增其他種不同口味的Pizza,用不同的原料工廠來達成
:::

ChinesePizzaFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ChinesePizzaFactory extends PizzaFactory {
protected Pizza createPizza(String style) {
Pizza pizza = null;

// 使用了中式原料工廠
PizzaIngredientFactory ingredientFactory = new ChineseIngredientFactory();

if(style.equals("Chinese")) {
pizza = new ChinesePizza(ingredientFactory);
pizza.setName("Chinese style pizza");
}

return pizza;
}
}

最後在Main.java中來製作披薩:

Main.java

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
PizzaFactory AmericaPizzaFactory = new AmericaPizzaFactory();

AmericaPizzaFactory.orderPizza("America");

PizzaFactory ChinesePizzaFactory = new ChinesePizzaFactory();

ChinesePizzaFactory.orderPizza("Chinese");
}
}

可以看到抽象工廠方法可以幫我們把相關產品集結起來(上述例子為原料們),可以產生出不同組的原料類別們,另外抽象工廠通常還會再搭配工廠方法,來建立其產品(披薩們,各個披薩會用到各個不同的抽象工廠所建立的原料工廠),如同該連結所做的->連結