1. 程式人生 > >設計模式筆記:工廠模式

設計模式筆記:工廠模式

這篇文章總結的主要是工廠方法和抽象工廠,順帶簡單工廠這種程式設計習慣

一、簡單工廠

簡單工廠並不算是一種設計模式,他更像一種程式設計習慣,並沒有嚴格的遵守開放關閉原則,而且他僅僅只是把要改變的部分跟不變的部分分離開,但是把具體產品的創造過程封裝起來,客戶端程式猿就不需要直接操作一堆具體子類,在知道最少的情況下達到目標,不必考慮這個類是怎麼被創建出來的,降低了程式的耦合。

我們看看wiki百科的一段話:普通的工廠方法模式通常伴隨著物件的具體型別與工廠具體型別的一一對應,客戶端程式碼根據需要選擇合適的具體型別工廠使用。然而,這種選擇可能包含複雜的邏輯。這時,可以建立一個單一的工廠類,用以包含這種選擇邏輯,根據引數的不同選擇實現不同的具體物件。這個工廠類不需要由每個具體產品實現一個自己的具體的工廠類,所以可以將工廠方法設定為靜態方法。 而且,工廠方法封裝了物件的建立過程。如果建立過程非常複雜(比如依賴於配置檔案或使用者輸入),工廠方法就非常有用了。

假設Pizza為產品抽象,它下層有各種具體子類Pizza,當客人預定Pizza的時候,提供一個類圖:


看起來就像是SimpleFactory託管了建立過程

public abstract class Pizza {
	public void prepare(){System.out.println("正在準備Pizza...");}
	public void bake(){System.out.println("正在烘培...");}
	public void cut(){System.out.println("正在切片...");}
	public void box(){System.out.println("正在裝箱...");}
}

public class PepperoniPizza extends Pizza {
	@Override
	public void prepare() {System.out.println("正在準備PepperoniPizza...");}
	@Override
	public void bake() {System.out.println("正在烘培PepperoniPizza...");}
	@Override
	public void cut() {System.out.println("正在切片PepperoniPizza...");}
	@Override
	public void box() {System.out.println("正在裝箱PepperoniPizza...");}
}

其他Pizza的子類省略,基本一樣
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 {
	private SimplePizzaFactory factory;
	public PizzaStore(SimplePizzaFactory factory){
		this.factory = factory;
	}
	
	public Pizza orderPizza(String type){
		Pizza pizza;
		
		pizza = factory.createPizza(type);
		
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		
		return pizza;
	}
}

能注意到的是,即使他僅僅是把建立的程式碼搬到了另外一個地方(其實實質也是這樣子),看上去問題並沒有解決,但需要考慮到加入建立的邏輯非常複雜,而我們需要給客戶程式設計師提供方便,這樣的方法就很有力量了,一般來說簡單工廠是宣告為靜態的,這樣子的缺點就是不能通過繼承來改變建立方法的行為

優點:

1、責任分割,客戶端程式猿直接被免除了建立產品的責任,決定在什麼時候建立子類

2、對客戶端來說,減少了記憶建立產品邏輯過程的記憶負擔

3、通過引入配置檔案,可以在不修改任何客戶端程式碼的情況下更換和增加新的具體產品類,在一定程度上提高了系統的靈活性

缺點:

1、除了無法繼承,但是邏輯複雜,一旦不能正常工作,整個系統都要受到影響

2、邏輯過於複雜,產品過多的時候,產品維護的花銷較大

所以,當我們考慮使用簡單工廠的時候,必須要考慮到它的執行邏輯、產品數目規模是不大的

二、工廠方法(Factory Method)

首先看看工廠方法的定義:工廠方法定義一個用於建立物件的介面,讓子類去決定例項化哪一個類,使其延遲例項化,也叫虛構造器(Virtual Constructor),這個名字更針對C++

創造工廠方法的動機,下面這段話來自《設計模式》:

考慮這樣一個應用框架,它可以向用戶顯示多個文件。在這個框架中,兩個主要的抽象是Application和Document。這兩個類都是抽象的,客戶必須通過他們的子類來做以應用相關的實現。例如,為建立一個繪圖應用,我們定義類DrawingApplication和DrawingDocument。Application負責管理Document並根據需要建立他們——例如,使用者從選單中選擇open或者new的時候。因為被例項化的特定Document子類是與特定應用相關的,所以Application類不可能預測到哪一個Document子類將被例項化——Application僅僅知道一個新的文件被建立,而不知道哪一種Document被建立。這就產生了一個尷尬的局面:框架必須被例項化,但是他只知道不能被例項化的抽象類。

這段話包含了一個重要的資訊:工廠方法讓子類決定要例項化的類,它本身並不知道要例項化哪一個

關於這句話:《Head First 設計模式》中這樣闡釋:所謂的“決定”,並不是指模式允許子類本身在執行時做決定,而是指在編寫建立者類的時候,不需要知道實際建立的產品是哪一個。選擇了使用哪一個子類,自然就決定了實際建立的產品是什麼

看看工廠方法的類圖:


我們試試把上面Pizza的例子改寫:簡單工廠允許我們生產cheese、viggie、clam、prpperoni型別的pizza,這都是把變化的部分封裝到SimpleFactory裡面去,現在需求變成這樣,口味還是這樣子的口味,但是我們需要不同的Pizza風格——NewYork的和Chicago風格的,每種風格下有這些不同的口味:(引數化工廠方法)


下面是程式碼:注意跟簡單工廠比較,思考哪裡讓“子類決定要例項化的類”

這是實體類,只列舉Pizza抽象類跟兩個風格類,其他不一一列舉

public abstract class Pizza {
	public void prepare(){System.out.println("正在準備Pizza...");}
	public void bake(){System.out.println("正在烘培...");}
	public void cut(){System.out.println("正在切片...");}
	public void box(){System.out.println("正在裝箱...");}
}

public class NYStyleVeggiePizza extends Pizza {
	@Override
	public void prepare() {System.out.println("正在準備NYStyleVeggiePizza...");}
	@Override
	public void bake() {System.out.println("正在烘培NYStyleVeggiePizza...");}
	@Override
	public void cut() {System.out.println("正在切片NYStyleVeggiePizza...");}
	@Override
	public void box() {System.out.println("正在裝箱NYStyleVeggiePizza...");}
}

public class ChicagoStyleClamPizza extends Pizza {
	@Override
	public void prepare() {System.out.println("正在準備ChicagoStyleClamPizza...");}
	@Override
	public void bake() {System.out.println("正在烘培ChicagoStyleClamPizza...");}
	@Override
	public void cut() {System.out.println("正在切片ChicagoStyleClamPizza...");}
	@Override
	public void box() {System.out.println("正在裝箱ChicagoStyleClamPizza...");}
}
接下來是工廠:
public abstract class PizzaStore {
	public Pizza orderPizza(String type){
		Pizza pizza;
		
		//工廠只知道什麼時候去建立子類,但是不知道建立什麼子類
		pizza = createPizza(type);
		
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		
		return pizza;
	}
	//交給子類去實現
	protected abstract Pizza createPizza(String type);
}

public class NYStylePizzaStore extends PizzaStore {

	@Override
	public Pizza createPizza(String type) {
		Pizza pizza = null;
		
		if(type.equals("cheese")){pizza = new NYStyleCheesePizza();
		}else if(type.equals("pepperoni")){pizza = new NYStylePepperoniPizza();
		}else if(type.equals("clam")){pizza = new NYStyleClamPizza();
		}else if(type.equals("veggie")){pizza = new NYStyleVeggiePizza();}
		
		return pizza;	
	}
}

public class ChicagoStylePizzaStore extends PizzaStore {

	@Override
	public Pizza createPizza(String type) {
		Pizza pizza = null;
		
		if(type.equals("cheese")){pizza = new ChicagoStyleCheesePizza();
		}else if(type.equals("pepperoni")){pizza = new ChicagoStylePepperoniPizza();
		}else if(type.equals("clam")){pizza = new ChicagoStyleClamPizza();
		}else if(type.equals("veggie")){pizza = new ChicagoStyleVeggiePizza();}
		
		return pizza;	
	}
}

可以自己寫一個main方法去實現看看結果

我們來看看工廠模式的優點:《Graphic Design Patterns》

1、在工廠方法模式中,工廠方法用來建立客戶所需要的產品,同時還向客戶隱藏了哪種具體產品類將被例項化這一細節,使用者只需要關心所需產品對應的工廠,無須關心建立細節,甚至無須知道具體產品類的類名。
2、基於工廠角色和產品角色的多型性設計是工廠方法模式的關鍵。它能夠使工廠可以自主確定建立何種產品物件,而如何建立這個物件的細節則完全封裝在具體工廠內部。工廠方法模式之所以又被稱為多型工廠模式,是因為所有的具體工廠類都具有同一抽象父類。
3、使用工廠方法模式的另一個優點是在系統中加入新產品時,無須修改抽象工廠和抽象產品提供的介面,無須修改客戶端,也無須修改其他的具體工廠和具體產品,而只要新增一個具體工廠和具體產品就可以了。這樣,系統的可擴充套件性也就變得非常好,完全符合“開閉原則”。

缺點:

1、在新增新產品時,需要編寫新的具體產品類,而且還要提供與之對應的具體工廠類,系統中類的個數將成對增加,在一定程度上增加了系統的複雜度,有更多的類需要編譯和執行,會給系統帶來一些額外的開銷。
2、由於考慮到系統的可擴充套件性,需要引入抽象層,在客戶端程式碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度,且在實現時可能需要用到DOM、反射等技術,增加了系統的實現難度。

其實工廠方法有他的變種形式:他可以不是類,而是在一系列的Product具體子類中用static的形式定義生成這個子類的工廠方法,同時私有化Product子類的構造方法,這樣子跟單例有點像,但這樣就無法體現讓工廠子類去確定例項化的系列具體產品類這個優點(尤其在於引數化工廠方法上),試考慮以上的Pizza例子,把工廠方法轉換成方法形式放在具體子類內部。視乎情況,我們還可以使用內部類去建立工廠方法,很多時候並不是想Pizza那樣具有相同的風格的一系列子類,而是每個產品都是獨立的,《Thinging in Java》中在內部類這一章節曾經提及過使用內部類去實現工廠方法,這裡總結一下,直接就工廠方法的Pizza改寫,上程式碼:

先把PizzaStore的type引數去掉,因為這裡並不需要建立風格系列的產品,當然,你也可以這樣子幹

public abstract class PizzaStore {
	public Pizza orderPizza(){
		Pizza pizza;
		
		//工廠只知道什麼時候去建立子類,但是不知道建立什麼子類
		pizza = createPizza();
		
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		
		return pizza;
	}
	//交給子類去實現
	protected abstract Pizza createPizza();
}
其次是在產品的內部實現工廠方法,這樣子做的優點就是安全,安全,安全,而且可以讓工廠託管子類被創造出來後的邏輯(prepare、bake、cut、box)
public abstract class Pizza {
	public void prepare(){System.out.println("正在準備Pizza...");}
	public void bake(){System.out.println("正在烘培...");}
	public void cut(){System.out.println("正在切片...");}
	public void box(){System.out.println("正在裝箱...");}
}

public class CheesePizza extends Pizza {
	private CheesePizza(){}
	@Override
	public void prepare() {System.out.println("正在準備CheesePizza...");}
	@Override
	public void bake() {System.out.println("正在烘培CheesePizza...");}
	@Override
	public void cut() {System.out.println("正在切片CheesePizza...");}
	@Override
	public void box() {System.out.println("正在裝箱CheesePizza...");}
	public static PizzaStore cheeseFactory = new PizzaStore() {
		@Override
		protected Pizza createPizza() {
			return new CheesePizza();
		}
	};
}
其他的子類都一樣,使用內部類實現了factory的方法,然後寫一下main方法:
public class test {
	public static void main(String[] args) {
		CheesePizza.cheeseFactory.orderPizza();
		System.out.println("------------------------------");
		ClamPizza.clamFactory.orderPizza();
		System.out.println("------------------------------");
		PepperoniPizza.pepperoniFactory.orderPizza();
		System.out.println("------------------------------");
		VeggiePizza.veggieFactory.orderPizza();
		System.out.println("------------------------------");
	}
}

你會發現,這樣子直接完成了orderPizza這個業務,而且有不錯的安全性

總結一下,在以下情況下可以使用工廠方法模式(From wiki):
1、一個類不知道它所需要的物件的類:在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品物件由具體工廠類建立;客戶端需要知道建立具體產品的工廠類。
2、一個類通過其子類來指定建立哪個物件:在工廠方法模式中,對於抽象工廠類只需要提供一個建立產品的介面,而由其子類來確定具體要建立的物件,利用面向物件的多型性和里氏代換原則,在程式執行時,子類物件將覆蓋父類物件,從而使得系統更容易擴充套件。
3、將建立物件的任務委託給多個工廠子類中的某一個,客戶端在使用時可以無須關心是哪一個工廠子類建立產品子類,需要時再動態指定,可將具體工廠類的類名儲存在配置檔案或資料庫中。

工廠方法的幾個效果:

1、工廠方法模式常見於工具包和框架中,在這些庫中可能需要建立客戶端程式碼實現的具體型別的物件。
2、平行的類層次結構中(一個類將它的一些職責委託給一個獨立的類的時候),常常需要一個層次結構中的物件能夠根據需要建立另一個層次結構中的物件。pizza與pizzastore
3、工廠方法模式可以用於測試驅動開發,從而允許將類放在測試中(這一點倒是沒有去研究過= =)

任何java程式都需要建立物件,如果想著工廠模式能不建立物件那就錯了,其實工廠模式好比一個柵欄,能把你的程式碼圍起來不至於維護他們時他們到處亂跑,物件的建立是現實的。

三、抽象工廠(Abstract Factory)

——提供一個建立一系列相關或者相互依賴物件的介面,而無需制定他們所需要的類

先看看類圖:


在以下情況考慮使用抽象工廠:《Graphic Design Patterns》

1、一個系統不應當依賴於產品類例項如何被建立、組合和表達的細節,這對於所有型別的工廠模式都是重要的。
2、系統中有多於一個的產品族,而每次只使用其中某一產品族。
3、屬於同一個產品族的產品將在一起使用,這一約束必須在系統的設計中體現出來。
4、系統提供一個產品類的庫,所有的產品以同樣的接口出現,從而使客戶端不依賴於具體實現。

當我們在Pizza上引入產品族時,dough、sauce、cheese、pepperoni、clam、veggies,並用這些產品來組裝一個Pizza——在之前的例子裡面,如果說NYStyleXXXXPizza之類屬於一個產品等級結構,那麼這些用來組合一個Pizza的原料就屬於一個產品族,在前面我們通過store來生產一個產品等級結構下的產品,而現在抽象工廠給我們的是,用一個產品族來生產一個產品。——一個針對產品等級結構、一個針對產品族,以下的例子除了上面的工廠方法,還引入了抽象工廠:

程式碼:

當然要提供一組原料介面:

public interface Dough {
//對於其他的介面也一樣被沒有特殊的操作
}
//工廠抽象裡面提供這一組原料的實現描述
public interface PizzaIngredientFactory {
	public Dough createDough();
	public Sauce createSauce();
	public Cheese createCheese();
	public Clams createClams();
	public Veggies[] createVeggies();
	public Pepperoni createPepperoni();
}
定義實體類,描述pizza需要的材料,以下提供了一個具體子類,還有其他的構造基本相同,具體子類的實現並沒有定義具體工廠,因為這裡思想是,你傳進來什麼工廠,它便用什麼工廠來生產原料,由選擇者去決定
public abstract class Pizza {
	public Dough dough;
	public Sauce sauce;
	
	public Veggies veggies[];
	public Cheese cheese;
	public Pepperoni pepperoni;
	public Clams clams;
	
	public abstract void prepare();
	public void bake(){System.out.println("正在烘培...");}
	public void cut(){System.out.println("正在切片...");}
	public void box(){System.out.println("正在裝箱...");}
}

public class NYStyleCheesePizza extends Pizza {
	PizzaIngredientFactory ingraedientFactory;
	public NYStyleCheesePizza(PizzaIngredientFactory ingraedientFactory) {
		this.ingraedientFactory = ingraedientFactory;
	}
	@Override
	public void prepare() {
		System.out.println("正在準備NYStyleCheesePizza...");
		dough = ingraedientFactory.createDough();
		sauce = ingraedientFactory.createSauce();
		cheese = ingraedientFactory.createCheese();
	}
	@Override
	public void bake() {System.out.println("正在烘培NYStyleCheesePizza...");}
	@Override
	public void cut() {System.out.println("正在切片NYStyleCheesePizza...");}
	@Override
	public void box() {System.out.println("正在裝箱NYStyleCheesePizza...");}
}

接下來實現抽象工廠來創造原料的產品族:這裡寫一個NY的工廠,Chicago的類似:

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
	@Override
	public Dough createDough() {return new ThinCrustDough();}
	@Override
	public Sauce createSauce() {return new MarinaraSauce();}
	@Override
	public Cheese createCheese() {return new ReggianoCheese();}
	@Override
	public Clams createClams() {return new FreshClams();}
	@Override
	public Pepperoni createPepperoni() {return new NYPepperoni();}
	@Override
	public Veggies[] createVeggies() {
		Veggies veggies[] = {new Garlic(),new Onion(),new Mushroom(),new RedPepper()};
		return veggies;
	}
}
我們可以看到具體工廠上實現了建立一整套原料的方法,我們使用它時,可以呼叫這些方法來生產一套產品族

在Store中,他們決定了pizza的生產過程,當然這裡也上上面工廠方法的體現處,這跟抽象工廠並不衝突,跟上面工廠方法不一樣的地方是具體Store多了一個抽象工廠的成員變數,因為要用它來創造原料

public abstract class PizzaStore {
	public Pizza orderPizza(String type){
		Pizza pizza;
		
		//工廠只知道什麼時候去建立子類,但是不知道建立什麼子類
		pizza = createPizza(type);
		
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		
		return pizza;
	}
	//交給子類去實現
	protected abstract Pizza createPizza(String type);
}

public class NYStylePizzaStore extends PizzaStore {

	@Override
	public Pizza createPizza(String type) {
		Pizza pizza = null;
		
		PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
		
		if(type.equals("cheese")){pizza = new NYStyleCheesePizza(ingredientFactory);
		}else if(type.equals("pepperoni")){pizza = new NYStylePepperoniPizza(ingredientFactory);
		}else if(type.equals("clam")){pizza = new NYStyleClamPizza(ingredientFactory);
		}else if(type.equals("veggie")){pizza = new NYStyleVeggiePizza(ingredientFactory);}
		
		return pizza;	
	}
}

抽象工廠的優點是:《Graphic Design Patterns》

抽象工廠模式隔離了具體類的生成,使得客戶並不需要知道什麼被建立。由於這種隔離,更換一個具體工廠就變得相對容易。所有的具體工廠都實現了抽象工廠中定義的那些公共介面,因此只需改變具體工廠的例項,就可以在某種程度上改變整個軟體系統的行為。另外,應用抽象工廠模式可以實現高內聚低耦合的設計目的,因此抽象工廠模式得到了廣泛的應用。
當一個產品族中的多個物件被設計成一起工作時,它能夠保證客戶端始終只使用同一個產品族中的物件。這對一些需要根據當前環境來決定其行為的軟體系統來說,是一種非常實用的設計模式。
增加新的具體工廠和產品族很方便,無須修改已有系統,符合“開閉原則”。

缺點:

在新增新的產品物件時,難以擴充套件抽象工廠來生產新種類的產品,這是因為在抽象工廠角色中規定了所有可能被建立的產品集合,要支援新種類的產品就意味著要對該介面進行擴充套件,而這將涉及到對抽象工廠角色及其所有子類的修改,顯然會帶來較大的不便。
開閉原則的傾斜性(增加新的工廠和產品族容易,增加新的產品等級結構麻煩)

參考資料:http://design-patterns.readthedocs.io/zh_CN/latest/index.html(圖說設計模式)、《Head First 設計模式》、《設計模式》、wiki百科