設計模式學習筆記之工廠模式

瑜戈發表於2019-01-19

本文講述一個披薩的誕生

我有一家披薩店,顧客來點了想吃的品種,然後我要準備材料、烘烤、剪下、幫顧客打包。這個過程用程式碼怎麼實現呢?

傳統方式

首先定義好準備、烘烤、剪下和打包這些動作

public abstract class Pizza {
    protected String name;

    public abstract void prepare();
    public void bake()
    {
        System.out.println(name+" baking;");
    }
    public void cut()
    {
        System.out.println(name+" cutting;");
    }
    public void box()
    {
        System.out.println(name+" boxing;");
    }
    public void setname(String name)
    {
        this.name=name;
    }
}
複製程式碼

然後定義一下都可以做哪些披薩種類

public class CheesePizza extend Pizza{
    @Override
    public void prepare(){
        super.setname("CheesePizza");
        System.out.println("CheesePizza");
    }
}
複製程式碼
public class GreekPizza extends Pizza {

    @Override
    public void prepare() {
        super.setname("GreekPizza");
        System.out.println("GreekPizza");
    }
}
複製程式碼

準備工作做完,就等著顧客來點餐了

public class OrderPizza {
    public OrderPizza() {
        String type = null;
        Pizza pizza = null;
        do {
            type = getType();
            if ("greek".equals(type)){
                pizza = new GreekPizza();
            }else if ("cheese".equals(type)){
                pizza = new CheesePizza();
            }else {
                break;
            }
            pizza.prepare();
            pizza.bake();
            pizza.cut();
            pizza.box();
        }while (true);
    }


    private String getType(){
        String type = null;
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("input pizza type:  ");
            type = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return type;
    }
}
複製程式碼

整個過程如下:
顧客根據披薩的種類進行點餐,然後根據型別進行建立披薩物件,進行準備、烘焙、剪下、打包

很明顯,這樣設計是有問題的。如果店裡有了新品,或者下架某種類披薩,就要修改OrderPizza類中根據顧客輸入來建立物件這段程式碼,顯然是違背上文提到的開放封閉原則。如下程式碼,現在我們已經知道哪些會改變,哪些不會改變,是時候使用封裝了。

//需要修改
if ("greek".equals(type)){
    pizza = new GreekPizza();
else if ("cheese".equals(type)){
    pizza = new CheesePizza();
}else {
    break;
}
//不需要修改
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
複製程式碼

簡單披薩工廠

現在最好將建立物件移到orderPizza()之外,但怎麼做呢?我們可以把建立披薩的程式碼移到另一個物件中,由這個新物件專職建立披薩。
我們稱這個新物件為“工廠”。
工廠(factory)處理建立物件的細節。一旦有了SimplePizzaFactory,orderPizza()就變成此物件的客戶。當需要披薩時,就叫披薩工廠做一個。那些orderPizza()方法需要知道希臘披薩或者蛤蜊披薩的日子一去不復返了。現在orderPizza()方法只關心從工廠得到了一個披薩,而這個披薩實現了Pizza介面,所以它可以呼叫prepare()、bake()、cut()、box()來分別進行準備、烘烤、切片、裝盒。

SimplePizzaFactory是我們的新類,它負責為客戶建立披薩

public class SimpleFactoryPizza {
    public Pizza createPizza(String type){
        Pizza pizza = null;
        if ("greek".equals(type)) {
            pizza=new GreekPizza();
        }else if ("cheese".equals(type)) {
            pizza=new CheesePizza();
        }else if ("beef".equals(type)) {
            pizza=new BeefPizza();
        }
        return pizza;
    }
}
複製程式碼

新的order只需構造時傳入一個工廠,然後帶入訂單型別來使用工廠建立披薩,代替之前具體的例項化

public class OrderPizza {
    SimpleFactoryPizza factory;

    public OrderPizza(SimpleFactoryPizza factory) {
        this.factory = factory;
    }

    Pizza orderPizza(String type) {
        Pizza pizza = null;
        pizza = factory.createPizza(type);
        if (pizza != null) {
            pizza.prepare();
            pizza.bake();
            pizza.cut();
            pizza.box();
        }
        return pizza;

    }

}

複製程式碼

雖然看似程式碼同傳統方式一樣,但是我們已經將變化的程式碼抽取出來了,在OrderPizza中我們無需再次修改,此時我們已經將變化的和不變化的隔離開來了。

工廠模式

如果披薩店生意越來越好,考慮開幾家加盟店。身為加盟公司的經營者,你希望確保加盟店的運營質量,還希望各地的披薩有自己不同的區域特點。在推廣SimpleFactoryPizza時,發現加盟店採用的統一的工廠建立的披薩,但是其他部分卻開始採用自創的流程,比如烘烤方式,包裝方式等等。那麼如果建立一個框架,約束關鍵步驟的同時又能保持一定的彈性呢?

修改給披薩店使用的框架

有一個辦法可以讓披薩製作活動侷限於OrderPizza類,同時讓不同的店還擁有自己的特色。
所要做的事情就是把createPizza()方法放回到OrderPizza中,不過得將它設定成抽象方法,然後為每個店鋪建立一個OrderPizza的子類。
來看下修改後的OrderPizza:

public abstract class OrderPizza {
    public Pizza orderPizza(String type){
        Pizza pizza;
        //將建立方法從工廠物件中移回OrderPizza中
        pizza = createPizza(type);
        //沒變過
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }
    //建立工廠方法改為抽象的,方便子類修改
    abstract Pizza createPizza(String type);
}
複製程式碼

現在已經有一個OrderPizza作為父類,讓不同區域的店鋪來繼承OrderPizza,每個子類各自決定製作什麼風味的披薩。

允許子類(不同店鋪)做決定

OrderPizza已經有一個不錯的訂單系統,由orderPizza()負責處理訂單,而你希望所有加盟店對於訂單的處理都能一致。
各個區域披薩店之間的差異在於他們製作披薩的風味(紐約披薩的薄脆、芝加哥披薩的餅厚等),我們現在要讓現在createPizza()能夠應對這些變化來負責建立正確種類的披薩。做法是讓OrderPizza的各個子類負責定義自己的createPizza()方法。所以我們會得到一些OrderPizza具體的子類,每個子類都有自己的披薩變體,而仍然適合OrderPizza框架,並使用除錯好的orderPizza()方法。

// 如果加盟店為顧客提供紐約風味的披薩,就使用NyStyleOrderPizza,
// 因為此類的createPizza()方法會建立紐約風味的披薩
public class NyStyleOrderPizza extends OrderPizza{
 
    //子類自己定義建立披薩方法
    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;
        if (type.equals("cheese")) {
            pizza = new NyStyleCheesePizza();
        } else if (type.equals("veggie")) {
            pizza = new NyStyleVeggiePizza();
        }
        return pizza;
    }
}
 
// 類似的,利用芝加哥子類,我們得到了帶芝加哥原料的createPizza()實現
public class ChicagoStyleOrderPizza extends OrderPizza{
 
    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;
 
        if (type.equals("cheese")) {
            pizza = new ChicagoCheesePizza();
        } else if (type.equals("veggie")) {
            pizza = new ChicagoVeggiePizza();
        }
        return pizza;
    }
}
複製程式碼

現在問題來了,OrderPizza的子類終究只是子類,如何能夠做決定?在NyStyleOrderPizza類中,並沒有看到任何做決定邏輯的程式碼。
關於這個方面,要從OrderPizza類的orderPizza()方法觀點來看,此方法在抽象的OrderPizza內定義,但是隻在子類中實現具體型別。
orderPizza()方法對物件做了許多事情(例如:準備、烘烤、切片、裝盒),但由於Pizza物件是抽象的,orderPizza()並不知道哪些實際的具體類參與進來了。換句話說,這就是解耦(decouple)!
當orderPizza()呼叫createPizza()時,某個披薩店子類將負責建立披薩。做哪一種披薩呢?當然是由具體的披薩店決定。
那麼,子類是實時做出這樣的決定嗎?不是,但從orderPizza()的角度看,如果選擇在NyStyleOrderPizza訂購披薩,就是由這個子類(NyStyleOrderPizza)決定。嚴格來說,並非由這個子類實際做“決定”,而是由“顧客”決定哪一家風味的披薩店才決定了披薩的風味。
我們來看下如何呼叫:

public class test {
    public static void main(String[] args) {
        OrderPizza orderPizza = new NyStyleOrderPizza();
        Pizza pi = orderPizza.orderPizza("cheese");
        System.out.println("-------");
        Pizza pizza = orderPizza.orderPizza("veggie");
    }
}
複製程式碼

執行結果如下:

Preparing NyStyleCheesePizza baking;
Preparing NyStyleCheesePizza cutting;
Preparing NyStyleCheesePizza boxing;
-------
Preparing NyStyleVeggiePizza baking;
Preparing NyStyleVeggiePizza cutting;
Preparing NyStyleVeggiePizza boxing;
複製程式碼

本文來源:《head-first設計模式》

設計模式學習系列:
基本概念
裝飾者模式

相關文章