烘烤oo的精華
我們已經學了3個章節了,還沒回答關于new的問題,我們不應該針對實作程式設計,但是當我們每次使用new時,不正是在針對實作程式設計嗎?
當看到”new“時,就會想到”具體“
是的,當使用new時,你的确是在執行個體化一個具體類,是以用的确實是實作,而不是接口。這是一個好問題,你已經知道了代碼綁着具體類會使代碼更脆弱。更缺乏彈性。
Duck duck=new MallardDuck();
要使用接口讓代碼具有彈性 ,new 具體類 但是還是得建立具體類的執行個體。
當有一群相關的具體類時,通常會寫出這樣的代碼:
Duck duck;
if(picnic){
duck=new MallardDuck();
}else if(hunting){
duck=new DecoyDuck();
}else if(inBathTub){
duck=new RubberDuck();
}
有一大推不同的鴨子類,但是必須等到運作時,才知道執行個體化哪一個。
當看到這樣的代碼,一旦有變化或擴充,就必須重新打開這段代碼進行檢查和修改,通常這樣的修改過的代碼将造成部分系統更難維護和更新,而且也更容易犯錯。
但是,總是要建立對象吧!而java隻提供了一個new關鍵字建立對象,不是嗎?
new有什麼不對勁?
在技術上,使用new并沒有錯,畢竟這是java的基礎部分,真正的犯人是我們的老朋友”改變“,以及它是如何影響new的使用的。
針對接口程式設計,可以隔離掉以後系統可能發生的一大堆改變,為什麼呢?如果代碼是針對接口程式設計,那麼通過多态,他可以與任何新類實作該接口,但是,當代碼中使用大量的具體類時,等于是自找麻煩,因為一旦加入新的具體類,就必須修改代碼。
也就是說,你的代碼并非”對修改關閉“,想用新的具體類型來擴充代碼,就必須打開它。
是以,當遇到這樣的問題時,就應該回到oo設計原則去尋找線索,别忘了,我們的第一個原則用來處理改變,并幫助我們”找出會變化的方面,把他們從不變的部分分離出來".
認識變化的方面
假設你有一個pizza店,
Pizza orderPizza(){
Pizza pizza=new Pizza ();為了讓系統有彈性,我們很希望這是一個抽象類或接口,但如果這樣,這些類或接口就無法執行個體化
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
但是你需要更多的pizza類型,是以你增加一些代碼,來“決定合适的pizza類型”,然後在制造pizza。
Pizza orderPizza(String type){ 現在把pizza類型傳入
Pizza pizza;
if(type.equals("cheess" )){
pizza= new CheessPizza(); 注意這裡的具體pizza類型都必須實作Pizza接口
} else if (type.equals("greek")){
pizza= new GreekPizza();
}
. . .
pizza.prepare(); 每個子類都知道如何準備自己
但是壓力來自于增加更多的pizza類型
我們想要增加一些新類型的pizza和删除一些舊類型的pizza,就必須修改以上代碼。
很明顯地,如果執行個體化“某些“具體類,将使orderPizza()出問題,而且也無法讓orderPizza對修改關閉,但是,現在我們已經知道哪些會改變,哪些不會改變,該是使用封裝的時候了。
封裝建立對象的代碼
現在最好将建立對象移到orderPizza之外,但怎麼做呢?這個嘛!要把建立pizza的代碼移到另一個對象中,由這個新對象專職建立pizza。
pizza orderPizza(String type){
原先的建立對象的代碼已經從該方法中抽離,
這裡該怎麼寫呢?
把原先的建立對象的代碼移到新對象中,如果任何對象想要建立pizza,找這個新對象就對了。
我們稱這個新對象為”工廠“。
工廠(factory)處理建立對象的細節。一旦有了SimplePizzaFactory,orderPizza()就變成了此對象的客戶。當需要pizza時,就叫pizza工廠做一個。那些orderpizza()需要知道pizza類型的日子一去不複返了。現在orderPizza()隻關心從工廠得到了一個pizza,而這個pizza實作了Pizza接口,是以他可以調用prepare(),bake()等。
還有一些細節,比方說,原先在orderPizza()方法中建立代碼,現在怎麼寫? 現在我們來實作一個簡單的pizza factory。
先從工廠本身開始,我們要定義一個類,為所有的pizza建立對象的代碼,代碼向這樣:
public class SimplePizzaFactory //這是一個新類,他隻做一件事:幫他的客戶建立 pizza
{
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;
這樣做有什麼好處?似乎隻是把問題搬到另一個對象罷了,問題依然存在。
答:别忘了,
SimplePizzaFactory 有許多的客戶,雖然目前隻看到orderpizza方法是他的客戶,然後,可能還有pizzashopMenu(pizza店菜單)類,會利用這個工廠來取得pizza的價錢和描述。可能還有一個HomeDelivery(宅急送)類,會以與PizzaShop類不同的方式來處理Pizza。總而言之,這個類可以有很多的客戶。
是以,把建立pizza的代碼包裝進一個類,當以後實作改變時,隻需修改這個類即可。
别忘了,我們也正要把具體執行個體化的過程,從客戶的代碼中删除!
問:我曾看到一個類似的設計方式,把工廠定義為一個靜态的方法,這有何差别?
答:利用靜态方法定義一個簡單的工廠,這是很常見的技巧,常稱為靜态工廠。為何使用靜态方法?因為不需要使用建立對象的方法來執行個體化對象。但請記住,這樣也有缺點,不能通過繼承來改變建立方法的行為。
重做PizzaStore類
是修改客戶代碼的時候了,我們要做的是仰仗工廠來為我們建立pizza。
public class PizzaStore {
SimplePizzaFactory factory;// 為PizzaStore加上SimplePizzaFactory的引用
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();
//這裡是其他方法
定義簡單工廠
簡單工廠其實不是一個設計模式,反而更像一種程式設計習慣,但由于經常被使用,是以我們給他一個”Head First Pattern 榮譽獎“,有些開發人員的确是把這個程式設計習慣誤認為是”工廠模式“,當你下次和另一個開發人員無話可說的時候,這應該是打破沉默的一個不錯的話題。
不要因為簡單工廠不是一個”真正的“模式,就忽略的它的用法,讓我們來看看新的Pizza類圖:、

加盟披薩店
對象村披薩店經營有成,很多人都想加盟。身為加盟公司的經營者,希望確定加盟店營運的品質,是以希望這些店都是用你那些經過時間考驗的代碼。
但是區域的差異呢?每家加盟店可能想要提共不同風味的pizza(比方說,紐約,芝加哥,加州)。這受到了開店地點及pizz美食家口味的影響。
我們按照原先的思路做,利用SimplePizzaFactory,寫出三種不同的工廠,那麼各地加盟店都有合适的工廠可以使用。這是一種做法。
但是,你想要更多控制。。。。
在推廣SimpleFactory時,你會發現加盟店的确實采用你的工廠建立pizza,但是其他部分,卻開始采用他們自創的流程:烘烤的做法有些差異,不要切片,使用其他廠商的盒子等。
在想想這個問題,你真的希望能夠建立一個架構,把加盟店和建立pizza綁在一起的同時又保持一定的彈性。
在我們稍早的SimplePizzaFactory代碼之前,制作pizza的代碼綁在PizzaStore裡,但這麼做卻沒有彈性。那麼,該如何做才能魚與熊掌兼得呢?
給披薩店使用的架構
有個做法可以讓披薩制作活動局限于PizzaStore類,而同時又能讓這些加盟店依然可以自由地制作該區域的風味。
所要做的事情,就是把createPizza()方法放回到PizzaStore中,不過要把它設定成”抽象方法“,然後為每個區域風味建立一個PizzaStore的子類。
首先,讓我們來看看PizzaStore所做的改變。
在PizzaStore裡,工廠方法現在是抽象的。
現在已經有了一個PizzaStore作為超類,讓每個域類型(NYPizzaStore,ChicagePizzaStore,CaliForniaPizzaStore)都繼承這個PizzaStore,每個子類各自決定如何制造Pizza,讓我們看看這要如何進行。
允許子類做決定
别忘了,PizzaStore已經有一個不錯的訂單系統,由orderPizza()方法負責處理訂單,而你希望所有加盟店對于訂單的處理都能夠一緻。
各個區域pizz之間的差異在于他們制作pizza的風味,我們現在要讓createPizza()能夠應對這些變化來負責建立正确種類的pizza。做法是讓PizzaStore的各個子類負責定義自己的createPizza方法,是以我們會得到一些PizzaStore具體的子類。
我不明白,畢竟PizzaS的子類終究隻是子類,如何能做決定?
關于這個,我們要從PizzaS的orderPizza()方法來看,此方法是抽象的PizzaStore内定義,但是隻是在子類中實作具體類型。
PizzaStore
createPizza()
orderPizza();
現在,更進一步地,orderPizza()方法對Pizza對象做了許多事情,(如bake,cut等),但由于Pizza對象是抽象的,orderPizza并不知道哪些實際的具體類參與進來了。換句話說:就是解耦decouple。
讓我們開一家Pizza Store吧
開加盟店有他的好處,可以從PizzaStore免費獲得所有的功能,區域點隻需要繼承PizzaStore,然後提供createPizza()方法實作自己的Pizza風味即可。
這是紐約風味:
其他的2個類型的PizzaStore類似。
聲明一個工廠方法
原本是由一個對象負責所有具體類的執行個體化,現在通過對PizzaStore做一些小轉變,變成由一群子類來負責執行個體化,讓我們看的仔細些:
各個類的代碼:
注意Pizza類代碼,我們特意用了abstract,雖然裡面沒有abstract方法,我們不想讓他執行個體化。
測試類:
認識工廠方法模式
所有工廠模式都用了封裝對象的建立,工廠方法模式通過讓子類決定該建立的對象是什麼,來達到将對象建立的過程封裝的目的。讓我們來看看這些類圖:
另一個觀點:平行的類層級 。
我們看到,将一根orderPizza()方法和一個工廠方法聯合起來,就可以成為一個架構,除此之外,工廠方法将生産知識封裝進各個建立者,這樣的做法,也可以被視為一個架構。
讓我們看看這兩個平行的類層級,
定義工廠方法模式
定義了一個建立對象的接口,但由子類決定要執行個體化的類時哪一個,工廠方法讓類把執行個體化推遲到子類。
上面的圖值得仔細看看。
問:工廠方法和建立者是否總是抽象的?
不?可以定義一個預設的工廠方法來産生一些具體的産品,這麼一來,即使建立者沒有任何子類,依然可以建立産品。
一個很依賴的披薩店
下面是一個不使用工廠模式的pizzaStore版本,數一下,這個類所依賴的具體披薩對象有幾種。如果又加了一種加州風味的Pizza到這個店,那麼屆時又會依賴幾個對象?
看看對象依賴
當你直接執行個體化一個對象是,就是在依賴他的具體類。
我們把這個版本的披薩店和他依賴的對象畫成一張圖,應該是這樣:
很清楚地,代碼裡減少對于具體類的依賴是件“好事”,事實上,有一個oo設計原則就正式闡明了這一點;這個
原則叫做:依賴倒置原則(dependency inversion principle)
通則如下:
要依賴抽象,不要依賴具體類。
首先,這個原則聽起來很像是“針對接口程式設計,不針對實作程式設計”,不是嗎?的确很像是,然而這裡更強調“抽象”。
這個原則說明了:不能讓高層元件依賴低層元件,而且,不管高層或低層元件,“兩者”都應該依賴于抽象。
讓我們看看DependentPizzaStore圖,PizzaStore是“高層元件”,而披薩實作的是“低層元件”,很清楚地,PizzaStore依賴這些具體類。
現在,這個原則告訴我們,應該重寫代碼以便于我們依賴抽象類,而不依賴具體類。對于高層及低層子產品都應該如此。
但是怎麼做呢?我們來想想看怎樣在“非常依賴披薩店”實作中,應用這個原則 。。。
原則的應用
非常依賴披薩店的問題在于:它依賴每個披薩類型,因為他是在自己的orderPizza()方法中,執行個體化這些具體類的。
雖然我們建立了一個抽象,也就是Pizza,但我們任然在代碼中,實際地建立了具體的Pizza,是以,這個抽象沒什麼影響力。
如何在orderPizza()方法中,将這些執行個體化對象的代碼獨立出來?我們都知道,工廠方法剛好可以派上用場。
是以,應用工廠方法後,類圖看起來像這樣:
在應用工廠方法之後,你将注意到,高層元件(也就是PizzaStore)和低層元件(也就是這些Pizza)都依賴了Pizza抽象,想要遵循依賴倒置原則,工廠方法并非是唯一的技巧,但卻是最有威力的技巧之一。
究竟倒置在哪裡?
在依賴倒置原則中的倒置指的是和一般oo設計的思考方式完全相反。看看前一頁的圖,低層元件現在竟然依賴高層的抽象,同樣第,高層元件現在也依賴相同的抽象。前幾頁所繪制的依賴圖是由上到下的,現在卻倒置了。而且高層和低層現在都依賴這個抽象。
幾個指導方針幫助你遵循依賴倒置原則
1.變量不可以持有具體類的引用。
(如果使用new,就會持有具體類的引用,你可以改用工廠方法來避開這樣的做法。)
2.不要讓類派生自具體類。
(如果派生自具體類,你就會依賴具體類,請派生自一個抽象(接口或抽象類)。
3不要覆寫基類中已實作的方法。
(如果覆寫基類中以實作的方法,那麼你的基類就不是一個真正适合被繼承的抽象。基類中已實作的方法,應該由所有的子類共享。)
但是,等等,要完全遵守這些規則,那麼我連一個簡單的程式都寫不出來!
你說的沒錯。正如同我們的許多原則一樣,應該盡量達到這個原則,而不是随時都要遵循這個原則。
但是,如果深入體驗這些方針,将這些方針内化成你思考的一部分,那麼在設計時,你将知道何時有足夠的理由違反這樣的原則。比方說。如果有一個不像是會改變的類,那麼在代碼中直接執行個體化具體類也就沒什麼障礙。想想看,我們平常還不是在程式中不假思索的i執行個體化字元串對象嗎?就沒有違法這個原則?當然有!可以這麼做嘛?可以!為什麼,因為字元串不可能改變。
另一方面,如果某個類可能改變,你可以采用一些好的技巧(如工廠方法)來封裝改變。