天天看點

工廠模式factory pattern

 烘烤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類圖:、

工廠模式factory pattern

加盟披薩店

 對象村披薩店經營有成,很多人都想加盟。身為加盟公司的經營者,希望確定加盟店營運的品質,是以希望這些店都是用你那些經過時間考驗的代碼。

 但是區域的差異呢?每家加盟店可能想要提共不同風味的pizza(比方說,紐約,芝加哥,加州)。這受到了開店地點及pizz美食家口味的影響。

 我們按照原先的思路做,利用SimplePizzaFactory,寫出三種不同的工廠,那麼各地加盟店都有合适的工廠可以使用。這是一種做法。

工廠模式factory pattern

 但是,你想要更多控制。。。。

  在推廣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具體的子類。

工廠模式factory pattern

 我不明白,畢竟PizzaS的子類終究隻是子類,如何能做決定?

 關于這個,我們要從PizzaS的orderPizza()方法來看,此方法是抽象的PizzaStore内定義,但是隻是在子類中實作具體類型。

 PizzaStore

createPizza()

orderPizza();

現在,更進一步地,orderPizza()方法對Pizza對象做了許多事情,(如bake,cut等),但由于Pizza對象是抽象的,orderPizza并不知道哪些實際的具體類參與進來了。換句話說:就是解耦decouple。

工廠模式factory pattern

讓我們開一家Pizza  Store吧

  開加盟店有他的好處,可以從PizzaStore免費獲得所有的功能,區域點隻需要繼承PizzaStore,然後提供createPizza()方法實作自己的Pizza風味即可。

這是紐約風味:

其他的2個類型的PizzaStore類似。

 聲明一個工廠方法

 原本是由一個對象負責所有具體類的執行個體化,現在通過對PizzaStore做一些小轉變,變成由一群子類來負責執行個體化,讓我們看的仔細些:

工廠模式factory pattern

各個類的代碼:

注意Pizza類代碼,我們特意用了abstract,雖然裡面沒有abstract方法,我們不想讓他執行個體化。

測試類:

認識工廠方法模式

 所有工廠模式都用了封裝對象的建立,工廠方法模式通過讓子類決定該建立的對象是什麼,來達到将對象建立的過程封裝的目的。讓我們來看看這些類圖:

工廠模式factory pattern

另一個觀點:平行的類層級 。

 我們看到,将一根orderPizza()方法和一個工廠方法聯合起來,就可以成為一個架構,除此之外,工廠方法将生産知識封裝進各個建立者,這樣的做法,也可以被視為一個架構。

 讓我們看看這兩個平行的類層級,

工廠模式factory pattern

定義工廠方法模式

 定義了一個建立對象的接口,但由子類決定要執行個體化的類時哪一個,工廠方法讓類把執行個體化推遲到子類。

工廠模式factory pattern

上面的圖值得仔細看看。

問:工廠方法和建立者是否總是抽象的?

不?可以定義一個預設的工廠方法來産生一些具體的産品,這麼一來,即使建立者沒有任何子類,依然可以建立産品。

一個很依賴的披薩店

 下面是一個不使用工廠模式的pizzaStore版本,數一下,這個類所依賴的具體披薩對象有幾種。如果又加了一種加州風味的Pizza到這個店,那麼屆時又會依賴幾個對象?

看看對象依賴

 當你直接執行個體化一個對象是,就是在依賴他的具體類。

我們把這個版本的披薩店和他依賴的對象畫成一張圖,應該是這樣:

工廠模式factory pattern

  很清楚地,代碼裡減少對于具體類的依賴是件“好事”,事實上,有一個oo設計原則就正式闡明了這一點;這個

原則叫做:依賴倒置原則(dependency inversion principle)

通則如下:

  要依賴抽象,不要依賴具體類。

 首先,這個原則聽起來很像是“針對接口程式設計,不針對實作程式設計”,不是嗎?的确很像是,然而這裡更強調“抽象”。

這個原則說明了:不能讓高層元件依賴低層元件,而且,不管高層或低層元件,“兩者”都應該依賴于抽象。

讓我們看看DependentPizzaStore圖,PizzaStore是“高層元件”,而披薩實作的是“低層元件”,很清楚地,PizzaStore依賴這些具體類。

 現在,這個原則告訴我們,應該重寫代碼以便于我們依賴抽象類,而不依賴具體類。對于高層及低層子產品都應該如此。

  但是怎麼做呢?我們來想想看怎樣在“非常依賴披薩店”實作中,應用這個原則 。。。

原則的應用

  非常依賴披薩店的問題在于:它依賴每個披薩類型,因為他是在自己的orderPizza()方法中,執行個體化這些具體類的。

 雖然我們建立了一個抽象,也就是Pizza,但我們任然在代碼中,實際地建立了具體的Pizza,是以,這個抽象沒什麼影響力。

 如何在orderPizza()方法中,将這些執行個體化對象的代碼獨立出來?我們都知道,工廠方法剛好可以派上用場。

 是以,應用工廠方法後,類圖看起來像這樣:

工廠模式factory pattern

 在應用工廠方法之後,你将注意到,高層元件(也就是PizzaStore)和低層元件(也就是這些Pizza)都依賴了Pizza抽象,想要遵循依賴倒置原則,工廠方法并非是唯一的技巧,但卻是最有威力的技巧之一。

究竟倒置在哪裡?

 在依賴倒置原則中的倒置指的是和一般oo設計的思考方式完全相反。看看前一頁的圖,低層元件現在竟然依賴高層的抽象,同樣第,高層元件現在也依賴相同的抽象。前幾頁所繪制的依賴圖是由上到下的,現在卻倒置了。而且高層和低層現在都依賴這個抽象。

幾個指導方針幫助你遵循依賴倒置原則

1.變量不可以持有具體類的引用。 

      (如果使用new,就會持有具體類的引用,你可以改用工廠方法來避開這樣的做法。)

2.不要讓類派生自具體類。

   (如果派生自具體類,你就會依賴具體類,請派生自一個抽象(接口或抽象類)。

3不要覆寫基類中已實作的方法。

  (如果覆寫基類中以實作的方法,那麼你的基類就不是一個真正适合被繼承的抽象。基類中已實作的方法,應該由所有的子類共享。)

 但是,等等,要完全遵守這些規則,那麼我連一個簡單的程式都寫不出來!

 你說的沒錯。正如同我們的許多原則一樣,應該盡量達到這個原則,而不是随時都要遵循這個原則。

但是,如果深入體驗這些方針,将這些方針内化成你思考的一部分,那麼在設計時,你将知道何時有足夠的理由違反這樣的原則。比方說。如果有一個不像是會改變的類,那麼在代碼中直接執行個體化具體類也就沒什麼障礙。想想看,我們平常還不是在程式中不假思索的i執行個體化字元串對象嗎?就沒有違法這個原則?當然有!可以這麼做嘛?可以!為什麼,因為字元串不可能改變。

 另一方面,如果某個類可能改變,你可以采用一些好的技巧(如工廠方法)來封裝改變。

繼續閱讀