一、概述
工廠方法模式定義了一個創建對象的接口,但由子類決定要實例化的類是哪一個。工廠方法讓類把實例化推遲到子類。
二、解決問題
通常我們需要一個對象的時候,會想到使用new來創建對象
Tea tea = new MilkTea(); //使用了接口,代碼更有彈性,體現設計原則“對接口編程,而不是對實現編程”
當我們需要多個對象的時候,”對接口編程“的原則似乎還能派上用場
Tea tea;
if("milk".equals(type)){
tea = new MilkTea();
}else if("coffee".equals(type)){
tea = new CoffeeTea();
}else if("lemon".equals(type)){
tea = new LemonTea();
}
這里也體現了“對接口編程”的好處,運行時決定要實例化哪個對象,讓系統具備了彈性,但對於擴展性方面,我們就不敢恭維了。看以上代碼,當我們要新增對象或者要擴展時,不得不打開這份代碼進行檢查和修改。通常這樣的修改過的代碼會造成部分系統更難維護和更新,而且也容易犯錯。
為了有良好的擴展性,我們想到了另外一個設計原則”把變化的代碼從不變化的代碼中分離出來”。
假設我們要開一家燒餅店,我們每天會做各種口味的燒餅出售,做燒餅的程序包括准備原材料、和面、烘烤、切片、裝盒。
//燒餅店
public class ShaobingStore {
public Shaobing orderShaobing(String type){
Shaobing shaobing = null;
if("onion".equals(type)){
//洋蔥燒餅
shaobing = new OnionShaobing();
}else if("sour".equals(type)){
//酸菜燒餅
shaobing = new SourShaobing();
}else if("beef".equals(type)){
//牛肉燒餅
shaobing = new BeefShaobing();
}
//以上代碼會發生改變,當洋蔥燒餅不再出售時,我們會把創建洋蔥的代碼刪除,我們可能會新增新口味的燒餅
else if("pork".equals(type)){
//牛肉燒餅
shaobing = new PorkShaobing();
}
//對於制作燒餅的程序中,以下這些步驟是不變的
if(shaobing != null){
shaobing.prepare();
shaobing.cut();
shaobing.bake();
shaobing.box();
}
return shaobing;
}
}
對於上面代碼,我們使用”分離變化“的原則,把創建燒餅代碼封裝到一個類中,我們把它叫做工廠類,里面專門一個方法用來創建燒餅,如下所示:
package factorymethod.pattern;
public class SimpleShaobingFactory {
public Shaobing createShaobing(String type){
Shaobing shaobing = null;
if("onion".equals(type)){
//洋蔥燒餅
shaobing = new OnionShaobing();
}else if("sour".equals(type)){
//酸菜燒餅
shaobing = new SourShaobing();
}else if("beef".equals(type)){
//牛肉燒餅
shaobing = new BeefShaobing();
}
return shaobing;
}
}
改進后的燒餅店如下:
package factorymethod.pattern;
//燒餅店
public class ShaobingStore {
public Shaobing orderShaobing(String type){
Shaobing shaobing = null;
shaobing = factory.createShaobing(type);
//對於制作燒餅的程序中,以下這些步驟是不變的
if(shaobing != null){
shaobing.prepare();
shaobing.cut();
shaobing.bake();
shaobing.box();
}
return shaobing;
}
SimpleShaobingFactory factory;
public ShaobingStore(SimpleShaobingFactory factory){
this.factory = factory;
}
}
如上圖所示,不管以后燒餅的口味怎么變,燒餅店的代碼都不用變了,要擴展或者修改燒餅,我們只要更改創建燒餅的工廠類,也就是SimpleShaobingFactory 類,這就解開了燒餅店和燒餅的耦合,體現了“對擴展開放,對修改關閉”的設計原則。
其實上面的改進方案使用了一個沒有被真正冠名的設計模式“簡單工廠模式”,其類圖如下所示:

從上面的類圖來看,如果我們的燒餅店開在不同的地方,不同地方對洋蔥燒餅,酸菜燒餅要求的口味不一樣,北方人喜歡放辣椒,南方人喜歡清淡的,我們的燒餅店該怎么開呢?這就是工廠方法模式要幫我們解決的問題,工廠方法模式讓類把實例化推遲到子類,讓子類決定實例化的類是哪一個,將產品的“實現”從“使用”中解耦出來,讓系統同時具備了彈性和擴展性。簡單工廠不夠彈性,不能改變正在創建的產品(同一種類型的只有一個,拿洋蔥燒餅來說,全國各地的口味一樣,沒有辣與不辣的區分了)
三、結構類圖

四、成員角色
抽象創建者(Creator):定義了創建對象模板,實現了所有操縱產品的方法,除了工廠方法。具體創建者必須繼承該類,實現工廠方法。
具體創建者(ConcreteCreator):繼承抽象創建者,實現工廠方法,負責創建產品對象。
抽象產品(Product):定義了產品的共用資源,提供給子類繼承使用,某些方法可以做成抽象方法,強制子類實現。
具體產品(ConcreteProduct):繼承自抽象產品,實現父類的抽象方法,也可以覆蓋父類的方法,從而產生各種各類的產品。
五、應用實例
下面還是以開燒餅店為例,介紹如何在廣州和長沙開燒餅店,賣適合當地風味的燒餅,而且燒餅的種類和名稱一樣。
首先抽象燒餅店,也就是Creator
package factorymethod.pattern;
public abstract class ShaobingStore {
public Shaobing orderShaobing(String type){
Shaobing shaobing = createShaobing(type);
shaobing.prepare();
shaobing.cut();
shaobing.bake();
shaobing.box();
return shaobing;
}
//未實現的工廠方法
public abstract Shaobing createShaobing(String type);
}
第二步,創建抽象燒餅,也就是Product
package factorymethod.pattern;
public abstract class Shaobing {
//燒餅名稱
public String name;
//燒餅用的配料
public String sauce;
//面團
public String dough;
public void prepare(){
System.out.println("Prepareing " + name);
//和面
System.out.println("Kneading dough...");
//加配料
System.out.println("加配料:" + sauce);
}
//烤燒餅
public void bake(){
System.out.println("Bake for 25 minutes at 350C");
}
//切面團
public void cut(){
System.out.println("Cutting the dough into fit slices");
}
//打包
public void box(){
System.out.println("Place shaobing into official box");
}
}
第三步、創建廣州風味的燒餅(加番茄醬的洋蔥燒餅和牛肉燒餅),對應ConcreteProduct
package factorymethod.pattern;
public class GZOnionShaobing extends Shaobing{
public GZOnionShaobing(){
name = "廣州的洋蔥燒餅";
//配料
sauce = "番茄醬";
}
}
package factorymethod.pattern;
public class GZBeefShaobing extends Shaobing{
public GZBeefShaobing(){
name = "廣州的牛肉燒餅";
//配料
sauce = "番茄醬";
}
}
第四步、創建長沙風味的燒餅(加辣椒醬的洋蔥燒餅和牛肉燒餅),對應ConcreteProduct
package factorymethod.pattern;
public class CSOnionShaobing extends Shaobing{
public CSOnionShaobing(){
name = "長沙洋蔥燒餅";
//配料
sauce = "辣椒醬";
}
}
package factorymethod.pattern;
public class CSBeefShaobing extends Shaobing{
public CSBeefShaobing(){
name = "長沙牛肉燒餅";
//配料
sauce = "辣椒醬 ";
}
}
第五步、創建廣州燒餅店,對應ConcreteCreator
package factorymethod.pattern;
//廣州燒餅店
public class GZShaobingStore extends ShaobingStore{
@Override
public Shaobing createShaobing(String type) {
Shaobing shaobing = null;
if("onion".equals(type)){
shaobing = new GZOnionShaobing();
}else if("beef".equals(type)){
shaobing = new GZBeefShaobing();
}
return shaobing;
}
}
第六步、創建長沙燒餅店,對應ConcreteCreator
package factorymethod.pattern;
//長沙燒餅店
public class CSShaobingStore extends ShaobingStore{
@Override
public Shaobing createShaobing(String type) {
Shaobing shaobing = null;
if("onion".equals(type)){
shaobing = new CSOnionShaobing();
}else if("beef".equals(type)){
shaobing = new CSBeefShaobing();
}
return shaobing;
}
}
第七步、測試售出名字相同但風味不一樣的燒餅
package factorymethod.pattern;
public class TestShaobingStore {
public static void main(String[] args){
//在廣州開一個燒餅店
ShaobingStore gzStore = new GZShaobingStore();
//售出一個洋蔥燒餅
gzStore.orderShaobing("onion");
System.out.println("----------------------");
//在長沙開一個燒餅店
ShaobingStore csStore = new CSShaobingStore();
//售出一個洋蔥燒餅
csStore.orderShaobing("onion");
}
}
運行結果:

六、工廠方法特有的設計原則
如果我們之間在燒餅店中直接實例化一個燒餅,這種設計師依賴具體類的,類圖如下:

這種依賴具體類設計,擴展性、彈性、維護性都比較差。如果將實例化的代碼獨立出來,使用工廠方法,我們將不再依賴具體類了,請看如下類圖:

這就是我們要講的依賴倒置原則:要依賴抽象,不要依賴具體類。用依賴倒置原則設計的系統,使得對象的實現從使用中解耦,對象的使用是在Creator,實現卻在ConcreteCreator中,Creator只有Product的引用,Creator與ConcreteProduct松耦合,這種設計很強的擴展性、彈性和可維護性。
設計中使用以來倒置原則方法:
1、變量不可以持有具體類的引用(就是不能使用new,使用工廠方法)
2、不要讓類派生自具體類(繼承抽象或者實現接口)
3、不要覆蓋基類中已實現的方法
七、優點和缺點
1、優點
(1)、符合“開閉”原則,具有很強的的擴展性、彈性和可維護性。擴展時只要添加一個ConcreteCreator,而無須修改原有的ConcreteCreator,因此維護性也好。解決了簡單工廠對修改開放的問題。
(2)、使用了依賴倒置原則,依賴抽象而不是具體,使用(客戶)和實現(具體類)松耦合。
(3)、客戶只需要知道所需產品的具體工廠,而無須知道具體工廠的創建產品的過程,甚至不需要知道具體產品的類名。
2、缺點
(1)、一個具體產品對應一個類,當具體產品過多時會使系統類的數目過多,增加系統復雜度。
(1)、每增加一個產品時,都需要一個具體類和一個具體創建者,使得類的個數成倍增加,導致系統類數目過多,復雜性增加。
(2)、對簡單工廠,增加功能修改的是工廠類;對工廠方法,增加功能修改的是客戶端。
八、使用場合
1、當需要一個對象時,我們不需要知道該對象所對應的具體類,只要知道哪個具體工廠可以生成該對象,實例化這個具體工廠即可創建該對象。
2、類的數目不固定,隨時有新的子類增加進來,或者是還不知道將來需要實例化哪些具體類。
3、定義一個創建對象接口,由子類決定要實例化的類是哪一個;客戶端可以動態地指定工廠子類創建具體產品。
