《Head First設計模式》讀書筆記
相關代碼:Vks-Feng/HeadFirstDesignPatternNotes: Head First設計模式讀書筆記及相關代碼
- 除了new操作符外,還有更多製造對象的方法
- “實例化”這個活動不應該總是公開地進行
- 初始化經常造成“耦合”問題
實例化的問題
當看到“new”就會想到“具體”
- 使用new時,就是在針對實現編程,而非針對接口編程
代碼綁着具體類會導致代碼更脆弱
Duck duck = new MallardDuck();
根據運行時條件決定實例化的具體類,有變化或擴展時需要對原有代碼進行檢查和修改,導致該部分更難維護和更新
Duck duck;
if (picnic) {
duck = new MallardDuck();
} else if (hunting) {
duck = new DecoyDuck();
} else if (inBathTub) {
duck = new RubberDuck();
}
“new”沒有錯,錯在“改變”
- 如何將實例化具體類的代碼從應用中抽離,或者封裝起來,使它們不會干擾應用的其他部分
- “找出變化的方面,把它們從不變的部分分離出來”
本節用例
你是一個披薩店主人
你的初始代碼如下:
Pizza orderPizza() {
//為了讓系統有彈性,我們希望這是一個抽象類或接口,但如果這樣,無法直接實例化
Pizza pizza = new Pizza();
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
但你需要更多的披薩類型
Pizza orderPizza(String type) {
Pizza pizza = new Pizza();
// 根據披薩類型實例化正確的具體類
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("greek")) {
pizza = new GreekPizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
由於實例化具體類的緣故,當你新增或刪除某些種類的披薩時,你需要修改上述的orderPizza(),這違背了“對修改關閉”的原則。
簡單工廠模式
識別變化的部分
上例中不難看出,披薩的準備過程是不變的
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
而變化的部分是披薩的具體類型,即實例化特定披薩種類的部分
// 根據披薩類型實例化正確的具體類
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("greek")) {
pizza = new GreekPizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
}
所以我們需要考慮將這部分封裝起來
封裝創建對象的代碼
將創建對象的部分移到orderPizza()之外
- 創建披薩的代碼移到另一個對象中,由它負責專職創建披薩。
我們稱這個新對象為“工廠(factory)”
- 工廠處理創建對象的細節
- 有了工廠後,在需要披薩時,orderPizza只需要關心從工廠得到了一個披薩,而且其實現了Pizza接口,可以完成後續任務(prepare、bake、cut、box)
建立一個簡單披薩工廠
/**
* 只做一件事:為客户創建披薩
*/
public class SimplePizzaFactory {
/**
* 客户用此方法實例化新對象
* @param type 披薩類型
* @return 相應類型的披薩
*/
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;
}
}
Q:這麼做的好處是什麼?這似乎只是把問題從一個對象搬到另一個對象,但問題仍然存在。
A:SimplePizzaFactory可以有很多用户。把創建披薩的代碼包裝進一個類,當以後實現改變時,只需要修改這個類即可(而不用再找所有有這段代碼的地方再去修改)。
Q:把工廠定義成靜態的方法,與上述方法有何區別?
A:利用靜態方法定義一個簡單的工廠是很常見的技巧,被稱為靜態工廠。之所以使用靜態方法,是因為不需要使用創建對象的方法來實例化對象(即不需要實例化工廠對象,就能獲得產品)。這種方法也存在缺點,不能通過繼承來改變創建方法的行為。
定義簡單工廠
簡單工廠其實不是一個設計模式,反而是比較像一種編程習慣。
工廠方法模式
工廠行為的動態改變
當披薩店做大做強,出現了加盟店,而不同地區的加盟店又希望能提供不同風味的披薩……
創建多個工廠
我們可以利用SimplePizzaFactory寫出多個種類的工廠,從而通過不同的工廠獲得不同風味的披薩
質量控制
推廣SimplePizza時,你發現加盟店的確是採用了你的工廠創建披薩,但是其他部分卻開始採用他們自創的流程。
- 你希望建立一個框架,把加盟店和創建披薩捆綁在一起的同時又保持一定的彈性
- 而最早的設計中,創建披薩的代碼綁在PizzaStore中,卻又沒有彈性
能不能找一個保持彈性而遵循框架的方法呢
給披薩店使用的框架
解決方法時,將createPizza()方法放回PizzaStore中,但將其設置為“抽象方法”,然後為每個區域風味創建一個PizzaStore的子類
PizzaStore代碼如下:
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
public abstract Pizza createPizza(String type);
}
允許子類自己做決定
上面的PizzaStore相當於為子類提供了一套框架,各個店鋪以PizzaStore為基類,實現自己的createPizza()方法
所謂“子類自己做決定”
orderPizza()在抽象的PizzaStore中定義,但是隻在子類中實現具體類型,所以store並不知道是哪個子類將實際製作披薩- 更進一步,
orderPizza()對Pizza對象做了很多事情,但由於Pizza對象是抽象的,orderPizza()並不知道哪些實際的具體類參與進來了。即實現瞭解耦(decouple) - 當
orderPizza()調用createPizza()時,某個披薩店子類將負責創建披薩,具體的披薩類型也由披薩店來決定。
聲明一個工廠方法
abstract Product factoryMethod(String type)
工廠方法用來處理對象的創建,並將這樣的行為封裝在子類中。這樣,客户程序中關於超類的代碼和子類對象創建代碼解耦了
- 工廠方法是抽象的,所以依賴子類來處理對象的創建
- 工廠方法必須返回一個產品,超類中定義的方法,通常使用到工廠方法的返回值
- 工廠方法將客户(也就是超類中的代碼,例如
orderPizza(),和實際創建具體產品的代碼分割開來
認識工廠方法模式
- 所有工廠模式都用來封裝對象的創建。
- 工廠方法模式(Factory Method Pattern)通過讓子類決定該創建的對象是什麼,來達到將對象創建的過程封裝的目的。
其組成元素:
-
創建者類
- 抽象創建者類定義了一個抽象的工廠方法,讓子類實現此方法制造產品
- 創建者通常會包含依賴於抽象產品的代碼,而這些抽象產品由子類製造,創建者不需要真的知道在製造哪種具體產品
-
產品類
另一觀點:平行的類層級
定義工廠方法模式
HeadFirst設計模式4-工廠方法模式
工廠方法模式定義了一個創建對象的接口,但由子類決定要實例化的類是哪一個。工廠方法讓類把實例化推遲到子類。
- 工廠方法模式能夠封裝具體類型的實例化。
- “工廠方法”:抽象的Creator提供了一個創建對象的方法的接口,稱為工廠方法
理解“工廠方法讓子類決定要實例化的類”
- 此處的“決定”並不是指模式允許子類本身在運行時決定
- 而是指在編寫創建者類時,不需要知道實際創建的產品是哪一個,選擇了使用哪個子類,自然就決定了實際創建的產品是什麼
Q:當只有一個ConcreteCreator時,工廠方法模式有什麼優點?
A:儘管只有一個具體創建者,工廠方法模式仍然很有用。因為它幫助我們將產品的“實現”從“使用”中解耦,如果增加產品或者改變產品的實現,Creator並不會受到影響。(因為Creator與任何其他ConcreteProduct之間都不是緊耦合)
Q:工廠方法和創建者是否總是抽象的?
A:不,可以定義一個默認的工廠方法來產生某些具體的產品,這麼一來。即使創建者沒有任何子類,依然可以創建產品
Q:簡單工廠與工廠方法之間的差異
A:簡單工廠把全部的事情在一個地方都處理完了,然而工廠方法卻是創建一個框架,讓子類決定要如何實現
封裝變化
- 可以將創建對象的代碼封裝起來。
- 實例化具體類的代碼,很可能在以後經常需要變化
- 通過“工廠”技巧,封裝實例化的行為
- 工廠的好處
- 將創建對象的代碼集中在一個對象或方法中,避免代碼中的重複,並且以後更方便維護。
- 實例化對象時,只依賴接口而不是具體類。讓我們針對接口編程而非針對實現編程
- 讓代碼更有彈性,更好地應對擴展
- 雖然在工廠代碼中仍然不可避免使用具體類來實例化真正的對象,但這並不能理解為只是“自欺欺人”。對象的創建是現實的,如果不創建任何對象,就無法創建Java程序。然而,利用這個現實的知識,可將這些創建代碼的對象用柵欄圍起來,就像你把所有的羊毛堆到眼前一樣,一旦圍起來,就可以保護這些創建對象的代碼。如果讓創建對象的代碼亂報,就無法收集到這些“羊毛”。
依賴倒置原則
對象依賴
反例:一個很依賴的披薩店。store需要依賴很多披薩對象
當你直接實例化一個對象時,就是在依賴他的具體類
依賴倒置原則
HeadFirst設計原則5
依賴倒置原則(Dependency Inversion Principle):要依賴抽象,不要依賴具體類
- 類似“針對接口編程,不針對實現編程”,但更強調抽象
- 該原則説明:不能讓高層組件依賴底層組件(同時兩者都應該依賴於抽象)
- “高層”組件:由其他低層組件定義其行為的類
原則的應用
通過工廠方法對上述的非常依賴的披薩店進行改進
- 主要問題:披薩店依賴所有披薩類型,因為它是在自己的方法中實例化這些具體類型的
- 已經創建了一個抽象——Pizza
工廠方法並非是唯一的技巧,但卻是最有威力的技巧之一
“倒置”
依賴倒置原則中的“倒置”指的是和一般OO設計的思考方式完全相反
從上圖中可以看到
- 底層組件會依賴高層的抽象
- 高層組件也依賴相同的抽象
倒置思考方式:
- 設計產品時,從頂端開始,然後往下到具體類
- 例如:實現披薩時,先抽象化一個Pizza,再去讓各種具體類型的披薩實現該接口
- 再去設計工廠,工廠會依賴我們抽象上述的產品的抽象
這樣我們就倒置了一個工廠的設計
遵循此原則的指導方針
- 變量不可以持有具體類的引用
- 使用“new”就會持有具體類的引用
- 可通過改進這點
- 不要讓類派生自具體類
- 派生自具體類,就會依賴該具體類
- 派生自抽象(接口或抽象類)
- 不要覆蓋類中已實現的方法
- 如果覆蓋類已實現的方法,那麼你的基類就不是一個真正適合被繼承的抽象
- 基類已實現的方法應該由所有子類共享
正如同許多其他原則一樣,我們要做的時儘量到達此原則,而非隨時都要遵循
- 完全遵循
實戰中很難打出來寸步難行
當你深入體驗這些方針,並且內化成思考的一部分,設計時就會知道何時有足夠的理由違法這樣的原則。 - 例如,不會改變的類直接實例化也無大礙,比如字符串對象
抽象工廠模式
背景
我們已經通過工廠方法模式導入了新的框架,讓加盟店嚴格遵循我們的流程,現在我們需要考慮確保原料的一致,從而保證品控。
打算建造一家生產原料的工廠,將原料運送到各家加盟店。但是對於不同地區的店鋪,需要準備多組原料。
在店鋪擴增時,原料也會相應改變,所以需要考慮如何處理原料家族
如何處理原料家族
原料工廠
建立原料工廠
建造一個工廠來生產原料,即創建原料工廠的每一種原料
開始先為工廠定義一個接口,這個接口負責創建所有的原料。
public interface PizzaIngredientFactory {
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Veggies[] createVeggies();
public Clams createClam();
}
- 為每個區域建造一個工廠。
- 創建繼承自PizzaIngredientFactory的子類來實現每一個創建方法
- 實現一組原料類供工廠使用
- 例如ReggianoCheesse、RedPeppers、ThickCrust-Dough。這些類可以在合適的區域間共享
- 將這些組織起來
- 新的原料工廠整合進舊的PizzaStore代碼
重寫Pizza類
重寫Pizza類,將其中的prepare()方法改為抽象方法,由子類實現,從而使用特定的原料工廠去獲取原料
public class CheesePizza extends Pizza{
PizzaIngredientFactory ingredientFactory;
public CheesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
@Override
public void prepare() {
System.out.println("Preparing " + name);
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
}
}
- Pizza的代碼利用相關的工廠生產原料
- 生產原料依賴使用的工廠,Pizza類不關心原料,只知道如何製作披薩
- 實現了披薩和原料的解耦
披薩店的實現
public class NYPizzaStore extends PizzaStore{
public Pizza createPizza(String item) {
Pizza pizza = null;
PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
if (item.equals("cheese")) {
pizza = new CheesePizza(ingredientFactory);
pizza.setName("New York Style Cheese Pizza");
} else if (item.equals("pepperoni")) {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("New York Style Pepperoni Pizza");
} else if (item.equals("clam")) {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("New York Style Clam Pizza");
} else if (item.equals("veggie")) {
pizza = new VeggiesPizza(ingredientFactory);
pizza.setName("New York Style veggie Pizza");
}
return pizza;
}
}
回顧總結
一連串代碼的變化,其本質是:我們引入新類型的工廠,即所謂的抽象工廠,來創建披薩原料家族
通過抽象工廠所提供的接口,可以創建產品的家族,利用這個接口書寫代碼,我們的代碼將從實際工廠解耦,以便在不同上下文中實現各式各樣的工廠,製造出各種不同的產品
因為代碼從實際的產品中解耦,所以我們可以替換不同的工廠來取得不同的行為
定義抽象工廠方法
HeadFirst設計模式5-抽象工廠模式
抽象工廠模式提供一個接口,用於創建相關或依賴對象的家族,而不需要明確指定具體類
- 允許客户使用抽象的接口來創建一組相關的產品,而不需要知道實際產出的具體產品是什麼,客户從具體產品中被解耦
從PizzaStore的角度來看:
工廠方法是不是潛伏在抽象工廠裏面
在上述代碼中我們可以發現,抽象工廠的每個方法實際上看起來都像是工廠方法,每個方法被聲明成抽象,子類的方法覆蓋這些方法來創建某些對象。這不就是工廠方法嗎
- 的確如此,抽象工廠的方法經常以工廠方法的方式實現。
- 抽象工廠的任務是定義一個負責創建一組產品的接口。這個接口內的每個方法都負責創建一個具體產品,同時我們利用實現抽象工廠的子類來提供這些具體的做法
- 所以在抽象工廠中利用工廠方法實現生產方法時相當自然的做法
工廠方法 v.s. 抽象工廠
共同作用:將對象的創建封裝起來,使應用程序解耦,並降低其對特定實現的依賴
| 對比內容 | 工廠方法 | 抽象方法 |
|---|---|---|
| 使用的設計模式 | 創建型模式 | 創建型模式 |
| 關注的對象類型 | 專注於單個產品的創建 | 專注於一組相關產品的創建 |
| 目標 | 提供一個接口,用來創建對象,子類決定具體的實現 | 提供一個接口,來創建產品家族中的多個相關對象 |
| 解耦的方式 | 客户端與具體產品解耦,客户端只需要知道接口而不關心具體實現
- 通過子類來創建對象 - 子類決定具體類型,客户只需要知道所用的抽象類型 |
客户端與整個產品族解耦,客户端只需知道工廠接口,具體產品族的變化由工廠子類處理
- 提供一個用來創建一個產品家族的抽象類型,該類型的子類定義產品被生產的方法。 - 想使用工廠,必須先實例化它,然後將它傳入一些針對抽象類型所編寫的代碼中。 |
| 是否有多個產品 | 創建單一產品(通常是一個具體的類) | 創建多個相關產品,產品之間通常有依賴關係,屬於同一產品族 |
| 擴展性 | 如果需要新產品,可以通過繼承和重寫工廠方法來實現新類型的創建 | 擴展產品族時,可能需要修改抽象工廠的接口,增加或改變產品接口,但可以通過新子類來應對不同產品族的需求 |
| 代碼結構 | 客户端通過工廠方法來創建產品,工廠方法通常在具體類中實現 | 客户端通過抽象工廠來獲得產品,工廠方法在不同的子工廠類中實現 |
| 使用類 vs 使用對象 | 使用具體的工廠類,通過繼承來擴展產品創建方式,客户端依賴於工廠類的繼承層次 | 使用抽象工廠接口,通過組合多個相關產品對象來創建產品家族,客户端依賴於工廠接口而不是具體實現 |
| 繼承 vs 對象組合 | 依賴於繼承來實現產品的創建,不同的具體工廠類通過繼承抽象工廠來實現不同的創建方法 | 依賴於對象組合來實現產品的創建,不同的具體工廠類實現抽象工廠接口,並通過組合多個產品來實現產品創建 |
| 使用時機 | 當只需要創建單一產品,且可能需要在未來擴展或修改具體產品的創建方式時,使用工廠方法 | 當需要創建多個相關的產品,且這些產品有共同的特性或屬於同一產品家族時,使用抽象工廠模式 |
採用工廠方法:
採用抽象工廠
總結
OO基礎
- 抽象
- 封裝
- 多態
- 繼承
OO原則
- 封裝變化
- 多用組合,少用繼承
- 針對接口編程,不針對實現編程
- 為交互對象之間的鬆耦合設計而努力
- 對擴展開放,對修改關閉
- 依賴抽象,不要依賴具體類
OO模式
- 抽象工廠模式——提供一個接口,用於創建相關或依賴對象的家族,而不需要明確具體類
- 工廠方法模式——定義了一個創建對象的接口,但由於子類決定要實例化類是哪一個。工廠方法讓類把實例化推遲到子類。
要點:
- 所有的工廠都是用來封裝對象的創建
- 簡單工廠,雖然不是真正的設計模式,但仍不失為一個簡單的方法,可以將客户程序從具體類解耦
- 工廠方法使用繼承,把對象的創建委託給子類,子類實現工廠方法來創建對象
- 抽象工廠使用對象組合,對象的創建被實現在工廠接口所暴露出來的方法中
- 所有工廠模式都通過減少應用程序和具體類之間的依賴促進鬆耦合
- 工廠方法允許類將實例化延遲到子類進行
- 抽象工廠創建相關的對象家族,而不需要依賴它們的具體類
- 依賴倒置原則,指導我們避免依賴具體類型,而要儘量依賴抽象
- 工廠是很有威力的技巧,幫助我們針對抽象編程,而不要針對具體類編程