Chapter 1 設(shè)計模式入門
Joe上班的公司做了一套相當(dāng)成功的模擬鴨子游戲:SimUDuck壕探。游戲中出現(xiàn)各種鴨子怖侦,一邊游泳戲水(swim)虱肄,一邊呱呱叫(quack),注意是呱呱叫抖仅,而不是吱吱叫坊夫。此系統(tǒng)的內(nèi)部設(shè)計使用了標(biāo)準(zhǔn)的OO技術(shù)砖第,設(shè)計了一個鴨子超類(Superclass),并讓各種鴨子繼承此超類环凿。
但梧兼,如果以后又加入新的鴨子類型,比如誘餌鴨(DecoyDuck)智听,即不會飛也不會叫……還有很多羽杰,我們可以自己想得到。那么joe的噩夢來了到推,這種設(shè)計方式有一下幾種缺點:
- 代碼在多個子類中重復(fù)考赛;
- 運行時的行為不容易改變;
- 很難知道所有鴨子的全部行為莉测;
- 改變會牽一發(fā)而動全身颜骤,造成其他鴨子不想要的改變;
Joe認(rèn)識到繼承可能不是答案捣卤,Joe知道規(guī)格會常常改變复哆,每當(dāng)有新的鴨子子類出現(xiàn),他就要被迫檢查并盡可能覆蓋fly()方法和quark()方法……這簡直是無窮的噩夢腌零。
Joe的主管告訴他,這真是一個超笨的主意唆阿,這么一來重復(fù)的代碼會變多益涧,如果認(rèn)為覆蓋幾個方法就算是差勁,那么對于48的Duck的子類都要稍微修改一下飛行的行為驯鳖,又怎么說闲询?
不管你在何處工作,構(gòu)建些什么浅辙,用何種編程語言扭弧,在軟件開發(fā)上,一致伴隨你的哪個不變的真理就是需求變更记舆!幸運的是鸽捻,有一個設(shè)計原則收班,恰好適用于此狀況虚吟。
設(shè)計原則:找出應(yīng)用中可能需要變化之處,把他們獨立出來垦梆,不要和哪些不需要變化的代碼混在一起诊赊。
換句話說厚满,如果每次新的需求一來,都會使某方面的代碼發(fā)生變化碧磅,那么你就可以確定碘箍,這部分的代碼需要被抽出來遵馆,和其他穩(wěn)定的代碼有所區(qū)分。也就是說丰榴,把會變化的部分取出并封裝起來货邓,以便以后可以輕易地改動或拓展此部分,而不影響不需要變化的其他部分多艇。
設(shè)計原則:針對接口編程逻恐,而不是針對實現(xiàn)編程。
"針對接口編程" 真正的意思是 "針對超類型編程":這里的接口有多個含義峻黍,接口是一個概念复隆,也是一種java的interface構(gòu)造。"針對接口編程"關(guān)鍵就在多態(tài)姆涩。利用多態(tài)挽拂,程序可以針對超類型編程,執(zhí)行時會根據(jù)實際情況執(zhí)行到真正的行為骨饿,不會被綁死在超類型的行為上亏栈。
這句話可以更明確的說成變量的聲明類型應(yīng)該是超類型,通常是一個抽象類或者是一個接口宏赘。如此绒北,只要是具體實現(xiàn)此超類型的類所產(chǎn)生的對象,都可以指定給這個變量察署。這也意味著聲明類時不用理會以后執(zhí)行的真正對象類型闷游。
根據(jù)面向接口編程我們將鴨子的行為改成醬紫:
但Duck是不是也應(yīng)該設(shè)計為接口類?在本例中這么做是木有必要的贴汪,因為已經(jīng)將變化的部分抽離出來了脐往,Duck已經(jīng)是共有的屬性和方法了,所以不用扳埂。
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {
}
abstract void display();
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("All ducks float, even decoys!");
}
}
=====================
package headfirst.designpatterns.strategy;
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
public void display() {
System.out.println("I'm a real Mallard duck");
}
}
但這里MallardDuck的初始化仍舊依賴了實現(xiàn)业簿,因為quackBehavior是接口類型,所以我們可以在運行時隨意的指定不同的實現(xiàn)類
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
這樣就可以改變鴨子的行為啦:
Duck model = new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
設(shè)計原則:多用組合阳懂,少用繼承梅尤。
※ 策略模式
定義了算法族,分別封裝起來希太,讓它們之間可以互相替換克饶,此模式讓算法的變化獨立于使用算法的客戶。
回顧:OO原則(封裝誊辉、繼承矾湃、多態(tài)、抽象)
可參考:https://www.cnblogs.com/xuwendong/p/10607308.html & https://www.cnblogs.com/joyous-day/p/6226802.html
封裝--封裝主要是基于類的屬性堕澄,本類中的屬性私有化邀跃,即有private修飾詞來加以修飾霉咨,生成GET,SET方法來給外界調(diào)用拍屑。
繼承--java繼承是面向?qū)ο蟮淖铒@著的一個特征途戒。繼承是從已有的來中派生出新的類,新的類能吸收已有類的數(shù)據(jù)屬性和行為僵驰,并能擴(kuò)展新的能力喷斋。
多態(tài)--java的多態(tài)是指對非靜態(tài)方法的多態(tài),父類的引用來調(diào)用子類的方法蒜茴。
Override和Overload都是多態(tài)的表現(xiàn):Override是子類對父類的允許訪問的方法的實現(xiàn)過程進(jìn)行重新編寫, 返回值和形參都不能改變星爪;Overload是在一個類里面,方法名字相同粉私,而參數(shù)不同抽象--使用關(guān)鍵詞abstrace聲明的類叫做“抽象類”顽腾。如果一個類里包含了一個或多個抽象方法,這個類就指定為抽象類诺核〕ぃ“抽象方法”屬于一種不完整的方法,只含有一個聲明窖杀,沒有方法體漓摩。
Chapter 2 觀察者模式
恭喜貴公司獲選為敝公司建立下一代Internet氣象觀測站!該氣象站必須建立在我們專利申請中的WeatherData對象上入客,由WeatherData對象負(fù)責(zé)追蹤目前的天氣狀況(溫度幌甘、濕度、氣壓)痊项。我們希望貴公司能建立一個應(yīng)用,有三種布告板酥诽,分別顯示目前的狀況鞍泉、氣相統(tǒng)計以及簡單的預(yù)報。當(dāng)WeatherData對象獲得最新的測量數(shù)據(jù)時肮帐,是那種布告板必須實時更新咖驮。
這樣做的問題是針對實現(xiàn)編程而非接口、對于每一個新布告板都得修改代碼训枢、不能運行時動態(tài)增刪布告板托修、也沒有封裝變化的部分。
※ 觀察者模式
定義了對象之間的一對多依賴恒界,這樣一來睦刃,當(dāng)一個對象改變狀態(tài)時,它的所有依賴者都會收到通知并自動更新
實現(xiàn)方式有很多種十酣,其中一種比較常見的是醬紫的:
當(dāng)2個對象之間松耦合涩拙,他們依然可以交互际长,但是不太清楚彼此的細(xì)節(jié)。觀察者提供了一種對象設(shè)計兴泥,讓subject和observer之間松耦合工育。
關(guān)于觀察者的一切,主題只知道觀察者實現(xiàn)了某個接口(也就是observer接口)搓彻,主題不需要知道觀察者的具體類是誰如绸,做了些什么等其他細(xì)節(jié)。
任何時候我們都可以增加觀察者旭贬,因為主題唯一依賴的東西是一個實現(xiàn)observer接口的對象列表怔接,所以我們可以隨時增加觀察者。
設(shè)計原則:為了交互對象之間的松耦合設(shè)計而努力骑篙。
根據(jù)觀察者模式對氣象站的實現(xiàn)做了修改:
有一個地方比較好玩蜕提,就是布告板的init傳入了weather data用于增加觀察者:
public CurrentConditionsDisplay(Subject weatherData){
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
//只是把最近的溫度展示出來
@Override
public void show() {
System.out.println("Current conditions: "+tempreture+",F degrees and"+
humidity+"% humidity");
}
@Override
public void update(float temp, int humidity, float pressure) {
//我們把溫度和濕度保存起來,然后調(diào)用展示數(shù)據(jù)的方法
this.tempreture = temp;
this.humidity = humidity;
this.pressure = pressure;
show();
}
這也是解耦的一個方面靶端,如果做單元測試的時候不想監(jiān)聽weatherdata了的話谎势,為了不去改布告板的內(nèi)部代碼,把被監(jiān)聽的subject通過傳入構(gòu)造器的方式更靈活杨名。而且如果想要取消觀察會更方便脏榆。
JDK中的observer
Observable 是一個類,而不是一個接口台谍,這點是與上面subject不同的须喂。Observable類追蹤所有的觀察者,并通知他們趁蕊。
首先坞生,你需要利用擴(kuò)展Java.util.Observerable 接口 產(chǎn)生可觀察者類,然后掷伙,需要先調(diào)用setChanged()方法是己,標(biāo)記狀態(tài)已經(jīng)改變的事實;然后調(diào)用兩種notifyObservers()方法中的一個:notifyObservers()
或notifyObservers(Object arg)
觀察者同以前一樣任柜,實現(xiàn)了更新的方法卒废,但是方法的簽名不太一樣:update( Observers o ,Object arg); //當(dāng)通知時,此版本可以傳送任何的數(shù)據(jù)對象給每一個觀察者
如果你想推送數(shù)據(jù)給觀察者宙地,你可把數(shù)據(jù)當(dāng)做數(shù)據(jù)對象傳送給notifyObservers(Object arg) 方法摔认。否則,觀察者就必須從可觀察者中拉數(shù)據(jù)宅粥。
其中的setChanged()方法用來標(biāo)記狀態(tài)已經(jīng)改變的事實参袱,好讓notifyObservers()知道當(dāng)它被調(diào) 用時應(yīng)該更新觀察者。如果調(diào)用notifyObservers()之前沒有先調(diào)用setChanged(),觀察者就“不會”被通知蓖柔。從Observable源碼分析了解下:
private boolean changed = false;
//setChanged()方法把changed標(biāo)志設(shè) 為true辰企。
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
//notifyObservers() 只會在 changed標(biāo)為“true”時通知觀 察者。
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; I--)
((Observer)arrLocal[i]).update(this, arg);
}
通過setChanged()方法可以讓你在更新觀察者時况鸣,更適當(dāng)?shù)赝ㄖ^察者,這樣會使程序有更多的彈性牢贸。比如,如果沒有setChanged()方法镐捧,氣象站測量太過精確潜索, 以致于溫度計讀數(shù)每十分之一度就會更新,這會造成WeatherData對象持續(xù)不斷地通知觀察者懂酱,浪費了資源竹习。如果我們希望半度以上才更新,就可以在溫度差距到達(dá)半度時列牺,調(diào)用setChanged()(加個if語句判斷即可)進(jìn)行有效的更新整陌。 或許我們不會經(jīng)常用到此功能,但是把這樣的功能準(zhǔn)備好瞎领,需要時就可以馬上使用泌辫。如果此功能在某些地方對你有幫助,你可能也需要clearChanged()方法九默,將changed狀態(tài)設(shè)置回false震放。另外也有一個hasChanged()方法, 告訴你changed標(biāo)志的當(dāng)前狀態(tài)驼修。
我們來看下如何用pull的方式獲取數(shù)據(jù)殿遂,而不讓subject將自己的狀態(tài)push出去:
import java.util.Observable;
public class WeatherData extends Observable {
//濕度
private float temperature;
//溫度
private float humidity;
//氣壓
private float pressure;
public WeatherData() {
}
//測量結(jié)果改變時,只有先將changed狀態(tài)改為true時乙各,通知所有觀察者方法才會被調(diào)用
public void measurementsChanged(){
setChanged();
notifyObservers();
}
//模擬手動設(shè)置測量結(jié)果(濕度墨礁,溫度,氣壓)
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
====================
public class CurrentConditionsDisplay implements Observer, DisplayElement {
//濕度
private float temperature;
//溫度
private float humidity;
//氣壓
private float pressure;
//
private Observable observable;
//運用多態(tài)傳入其Observable子類對象即可
public CurrentConditionsDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
//現(xiàn)在自己去主題對象中拿數(shù)據(jù)
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherData){
WeatherData weatherData = (WeatherData) o;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
this.pressure = weatherData.getPressure();
display();
}
}
@Override
public void display() {
System.out.println("目前狀況:濕度為" + temperature + "RH耳峦; 溫度為:" + humidity + "度饵溅;氣壓為" + pressure + "帕斯卡");
}
}
當(dāng)然Java自帶的觀察者模式類和接口也有缺點,比如主題Observable是一個“類”而不是一個接口妇萄,我們必須設(shè)計一個子類來實現(xiàn)它,如果該子類還要繼承其他的父類咬荷,就無法做到了冠句,因為Java不支持多繼承,這限制了Observable的復(fù)用能力幸乒。而且setChanged方法被用protected關(guān)鍵字保護(hù)起來了懦底,只有你繼承了Observable類才能使用,無法將其組合到自己的對象中,違反了設(shè)計原則:多用組合聚唐,少用繼承丐重。
有木有發(fā)現(xiàn)這個比我們最開始依次調(diào)用update(temp, humidity, pressure)
要好很多?如果將來增加了屬性也不用改每個觀察者的方法杆查,只要改subject就可以了扮惦,這也是封裝變化的一個方面。
- 觀察者模式允許push(推)或拉(pull)的方式傳送數(shù)據(jù)亲桦。(最好是push)
有多個觀察者時崖蜜,不可以依賴特定的通知次序。
Chapter 3 裝飾者模式
歡迎來到星巴茲咖啡客峭,該公司是世界上以擴(kuò)張速度最快而聞名的咖啡連鎖店豫领。但是最近這家著名的咖啡公司遇到一個巨大的問題,因為擴(kuò)展速度太快了舔琅,他們準(zhǔn)備更新訂單系統(tǒng)等恐,以合乎他們的飲料供應(yīng)需求。
然后客戶購買咖啡時备蚓,可以要求在其中加入任何調(diào)料课蔬,例如:奶茶,牛奶星著,豆?jié){购笆。星巴茲根據(jù)業(yè)務(wù)需求會計算相應(yīng)的費用。這就要求訂單系統(tǒng)必須考慮到這些調(diào)料的部分虚循。
如果每加一種配料都算做一個新的class同欠,就會有一種犯了密集恐懼癥的感覺,完全就是“類爆炸”横缔。
這樣肯定是不可以的铺遂,那我們可以考慮把配料作為實例變量,cost會根據(jù)實例變量來計算價格茎刚。
到考慮設(shè)計模式就得想到之后的變化襟锐。
看起來很完美,也能滿足現(xiàn)有的業(yè)務(wù)需求膛锭,但是仔細(xì)思考一下粮坞,真的這樣設(shè)計不會出錯?回答肯定是會出錯初狰。
- 一旦調(diào)料的價格發(fā)生變化莫杈,會導(dǎo)致我們隊原有代碼進(jìn)行大改。
- 一旦出現(xiàn)新的調(diào)料奢入,我們就需要加上新的方法筝闹,并需要改變超類Beverage類中cost()方法。
- 如果星巴茲咖啡研發(fā)新的飲料。對于這些飲料而言关顷,某些調(diào)料可能并不合適糊秆,但是子類仍然會繼承那些本就不合適的方法,例如我就想要一杯水议双,加奶泡(hasWhip)就不合適痘番。
- 如果用戶需要雙倍的摩卡咖啡,又應(yīng)該怎么辦呢聋伦?
設(shè)計原則:類應(yīng)該對拓展開放夫偶,對修改關(guān)閉。
那么什么是開放觉增,什么又是關(guān)閉兵拢?開放就是允許你使用任何行為來拓展類,如果需求更改(這是無法避免的)逾礁,就可以進(jìn)行拓展说铃!關(guān)閉在于我們花費很多時間完成開發(fā),并且已經(jīng)測試發(fā)布嘹履,針對后續(xù)更改腻扇,我們必須關(guān)閉原有代碼防止被修改,避免造成已經(jīng)測試發(fā)布的源碼產(chǎn)生新的bug砾嫉。
綜合上述說法幼苛,我們的目標(biāo)在于允許類拓展,并且在不修改原有代碼的情況下焕刮,就可以搭配新的行為舶沿。如果能實現(xiàn)這樣的目標(biāo),帶來的好處將相當(dāng)可觀配并。在于代碼會具備彈性來應(yīng)對需求改變括荡,可以接受增加新的功能用來實現(xiàn)改變的需求。沒錯溉旋,這就是拓展開放畸冲,修改關(guān)閉。
那么有沒有可以參照的實例可以分析呢观腊?有邑闲,就在第二篇我們介紹觀察者模式時,我們介紹到可以通過增加新的觀察者用來拓展主題梧油,并且無需向原主題進(jìn)行修改苫耸。
我們是否需要每個模塊都設(shè)計成開放--關(guān)閉原則?不用婶溯,也很難辦到(這樣的人我們稱為“不用設(shè)計模式會死病”)。因為想要完全符合開放-關(guān)閉原則,會引入大量的抽象層迄委,增加原有代碼的復(fù)雜度褐筛。我們應(yīng)該區(qū)分設(shè)計中可能改變的部分和不改變的部分(第一設(shè)計原則),針對改變部分使用開放--關(guān)閉原則叙身。
現(xiàn)在來用裝飾者模式來改造一下星巴茲咖啡的計算:
這就需要一個新的設(shè)計思路渔扎。這里,我們將以飲料為主信轿,然后運行的時候以飲料來“裝飾”飲料晃痴。舉個栗子,如果影虎需要摩卡和奶泡深焙咖啡财忽,那么要做的是:拿一個深焙咖啡(DarkRosat)對象倘核、以摩卡(Mocha)對象裝飾它、以奶泡(Whip)對象裝飾它即彪、調(diào)用cost方法紧唱,并依賴委托將調(diào)料的價錢加上去。
- 裝飾者和被裝飾對象有相同的超類型隶校。
- 你可以用一個或多個裝飾者包裝一個對象漏益。
- 既然裝飾者和被裝飾對象有相同的超類型,所以在任何需要原始對象(被包裝的)的場合深胳,可以用裝飾過的對象代替它绰疤。
- 裝飾者可以在所委托被裝飾者的行為之前與/或之后,加上自己的行為舞终,以達(dá)到特定的目的轻庆。
- 對象可以在任何時候被裝飾,所以可以在運行時動態(tài)地权埠、不限量地用你喜歡的裝飾者來裝飾對象
什么是裝飾模式呢榨了?我們首先來看看裝飾模式的定義:
裝飾者模式動態(tài)地將責(zé)任附加到對象上。 若要擴(kuò)展功能攘蔽,裝飾者提供了比繼承更有彈性的替代方案龙屉。
下面我們用裝飾者模式改造一下星巴茲咖啡:
從類圖我們看到,CondimentDecorator擴(kuò)展自Beverage類满俗,用到了繼承转捕。這么做的重點在于,裝飾者和被裝飾者必須是一樣的類型唆垃,也就是有共同的父類五芝,這是相當(dāng)關(guān)鍵的地方。這里的繼承是達(dá)到“類型匹配”辕万,而不是利用繼承獲得“行為”枢步。
那么行為又是從哪里來的呢沉删?
當(dāng)我們將裝飾者和組件組合時,就是在加入新的行為醉途。所得到的新行為矾瑰,并不是繼承自父類,而是由組合對象得來的隘擎。也就是說殴穴,繼承Beverage類,是為了有正確的類型货葬,而不是繼承它的行為采幌。行為來自裝飾者和基礎(chǔ)組件,或與其他裝飾者之間的組合關(guān)系震桶。
因為使用對象組合休傍,就可以把所有奶茶和配料更有彈性地加以混合和匹配,非常方便尼夺。如果依賴?yán)^承尊残,那么類的行為只能在編譯時靜態(tài)決定,行為不是來自父類淤堵,就是子類覆蓋后的版本寝衫。反之,利用組合拐邪,可以把裝飾者混合使用慰毅,而且是在“運行時”!T住汹胃!
為什么不把Beverage設(shè)計成一個接口,而是抽象類呢东臀?
通常裝飾者模式是采用抽象類着饥,但是在Java中可以使用接口。盡管如此惰赋,我們都努力避免修改現(xiàn)有的代碼宰掉,所以,如果抽象類運作的好好地赁濒,還是別去修改它轨奄。
public abstract class Beverage {
String description="Unknown Beverage";
public String getDescription(){
return description;
}
public abstract double cost();
}
public abstract class CondimentDecorator extends Beverage {
//所有的調(diào)料裝飾者都必須重新實現(xiàn) getDescription()方法。
public abstract String getDescription();
}
public class Espresso extends Beverage {
public Espresso(){
//為了要設(shè)置飲料的描述拒炎,我 們寫了一個構(gòu)造器挪拟。記住, description實例變量繼承自Beverage1
description="Espresso";
}
public double cost() {
//最后击你,需要計算Espresso的價錢玉组,現(xiàn)在不需要管調(diào)料的價錢谎柄,直接把Espresso的價格$1.99返回即可。
return 1.99;
}
}
下面讓我們看一下調(diào)料的代碼:
public class Mocha extends CondimentDecorator {
/**
* 要讓Mocha能夠引用一個Beverage惯雳,采用以下做法
* 1.用一個實例記錄飲料谷誓,也就是被裝飾者
* 2.想辦法讓被裝飾者(飲料)被記錄在實例變量中。這里的做法是:
* 把飲料當(dāng)作構(gòu)造器的參數(shù)吨凑,再由構(gòu)造器將此飲料記錄在實例變量中
*/
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage=beverage;
}
public String getDescription() {
//這里將調(diào)料也體現(xiàn)在相關(guān)參數(shù)中
return beverage.getDescription()+",Mocha";
}
/**
* 想要計算帶摩卡的飲料的價格,需要調(diào)用委托給被裝飾者户辱,以計算價格鸵钝,
* 然后加上Mocha的價格,得到最終的結(jié)果庐镐。
*/
public double cost() {
return 0.21+beverage.cost();
}
}
然后來看下測試的代碼:
public class StarbuzzCoffe {
public static void main(String[] args) {
//訂購一杯Espresso恩商,不需要調(diào)料,打印他的價格和描述
Beverage1 beverage=new Espresso();
System.out.println(beverage.getDescription()+"$"
+beverage.cost());
//開始裝飾雙倍摩卡+奶泡咖啡
Beverage1 beverage2=new DarkRoast1();
beverage2=new Mocha(beverage2);
beverage2=new Mocha(beverage2);
beverage2=new Whip(beverage2);
System.out.println(beverage2.getDescription()+"$"
+beverage2.cost());
//
Beverage1 beverage3=new HouseBlend();
beverage3=new Soy(beverage3);
beverage3=new Mocha(beverage3);
beverage3=new Whip(beverage3);
System.out.println(beverage3.getDescription()+"$"
+beverage3.cost());
}
}
裝飾者的確會有很多對象必逆,但是它主要是給工廠類服務(wù)的怠堪,所以會把內(nèi)部封裝的很好防止使用者出錯。
JAVA的I/O也是典型的裝飾者模式
硬幣有正反兩面名眉,裝飾者模式也有一個“缺點”:利用裝飾者模式粟矿,常常造成設(shè)計中有大量的小類,數(shù)量實在太多损拢,可能會造成使用相關(guān)API(如java.io)程序員的困擾陌粹,不過知道了裝飾者的工作原理,以后就能很容易地辨別出它們的裝飾者類是如何組織的福压,以方便用包裝方式取得想要的行為掏秩。
Chapter 4 烘烤OO的精華
如果有一個披薩店,需要根據(jù)用戶點的披薩進(jìn)行加工荆姆,我們可以創(chuàng)建一個Pizza抽象類蒙幻,然后根據(jù)用戶的點單進(jìn)行動態(tài)實例化:
但這里的new是很依賴具體實現(xiàn)的,如果我們怎加了一個披薩類型胆筒,或者減少了都要改這里的orderPizza邮破。
那要怎么封裝變化呢?也就是把創(chuàng)建披薩的代碼封裝起來腐泻,讓一個對象專門負(fù)責(zé)創(chuàng)建pizza决乎,這就是創(chuàng)建披薩的工廠:
// 只做一件事情,幫它的客戶創(chuàng)建比薩派桩。
public class SimplePizzaFactory {
// 首先构诚,在這個工廠內(nèi)定義一個createPizza()方法。所有客戶用這個方法來實例化新對象铆惑。
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;
}
}
這樣做的話就可以讓這個工廠給很多其他的東西提供對象了范嘱,不僅僅是這家披薩店送膳,如果需要修改也只要改工廠就好。很多工廠的方法都是靜態(tài)的丑蛤,這樣就不用創(chuàng)建factory對象叠聋,但是不太好的是這個create的方法就寫死了不能通過繼承之類的覆寫。
用工廠改寫披薩店是醬紫的:
public class PizzaStore{
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory) {
this.factory=factory;
}
public Pizza orderPizza(string type) {
Pizza=pizza;
pizza=factory.CreatePizza(type);
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
但是如果由于經(jīng)營得當(dāng)受裹,開分店已經(jīng)是這個世界上最正常不過的事情了碌补,為了體現(xiàn)地方特色,我們希望我們開的分店(或是說加盟店)能夠加入當(dāng)?shù)氐奶厣珌碜雠_棉饶,所以厦章,我們把披薩的生產(chǎn)下放到每一個分店中。下面是我們重新設(shè)計的代碼:
public abstract class PizzaStore
{
public Pizza OrderPizza(string pizzaType)
{
Pizza pizza = new Pizza();
pizza = CreatePizza(pizzaType);
pizza.PrePare();
pizza.Bake();
pizza.Cut();
pizza.Box();
return pizza;
}
public abstract Pizza CreatePizza(string pizzaType);
}
public class NYPizzaStore : PizzaStore
{
public Pizza CreatePizza(string item)
{
if (item.Equals("cheese"))
return new NYCheesePizza();
else
return null;
}
}
工廠方法模式:定義了一個創(chuàng)建對象的接口照藻,但由子類決定要實例化的類時哪一個袜啃。工廠方法讓類把實例化推遲到子類,來達(dá)到將對象創(chuàng)建的過程封裝的目的幸缕。
在工廠模式里面群发,我們要弄清楚兩個重要的角色。
創(chuàng)建類(Creator)類
這是抽象創(chuàng)建者類发乔,它定義了一個抽象的工廠方法熟妓,讓子類實現(xiàn)此方法制造產(chǎn)品。創(chuàng)建者通常會包含依賴于抽象產(chǎn)品的代碼栏尚,而這些抽象產(chǎn)品偶子類制造滑蚯。創(chuàng)建者不需要真的知道在制造哪中具體產(chǎn)品。也就是這里的PizzaStore類抵栈。其中的CreatePizza()方法正是工廠方法告材,用來制造產(chǎn)品。產(chǎn)品類古劲,也就是這里的Pizza類斥赋。工廠生產(chǎn)產(chǎn)品。對PizzaStore來說产艾,產(chǎn)品就是Pizza疤剑。
這兩個類層級為什么是平行的:因為它們都有抽象類,而抽象類都有許多具體的子類闷堡,每個之類都有自己特定的實現(xiàn)隘膘。
簡單工廠和工廠方法的區(qū)別:
子類的確看起來很像簡單工廠。簡單工程把全部的事情杠览,在一個地方都處理完了弯菊,然而工廠方法卻是創(chuàng)建了一個框架,讓子類決定要如何實現(xiàn)踱阿。比方說管钳,在工廠方法中钦铁,orderPizza()方法提供了一個一般的框架,以便創(chuàng)建披薩才漆,orderPizza()方法依賴工廠方法創(chuàng)建具體類牛曹,并制造出實際的披薩〈祭模可通過繼承PizzaStore()類黎比,決定實際制造出的披薩是什么。簡單工廠的做法鸳玩,可以將對象創(chuàng)建封裝起來焰手,但是簡單工廠不具備工廠方法的彈性,因為簡單工程不能變更正在創(chuàng)建的產(chǎn)品怀喉。
依賴倒置原則 (Dependency Inversion Principle)
要依賴抽象,不要依賴具體類船响。不要讓高層組件依賴于底層組件躬拢,而且,不管高層或低層組件见间,“兩者”都應(yīng)該依賴于抽象聊闯。所謂“高層”組件,是由其他底層組件定義其行為的類米诉。
例如菱蔬,PizzaStore是個高層組件,因為它的行為是由披薩定義的:PizzaSotre創(chuàng)建所有不同的比薩對象史侣,準(zhǔn)備拴泌。烘烤。切片惊橱,裝盒蚪腐。而比薩本身屬于底層組件。
變量不可以持有具體類的引用税朴。如果使用new回季,就會持有具體類的引用。你可以改用工廠來避開這樣的做法正林。
不要讓類派生自具體類泡一。如果派生自具體類,你就會依賴具體類觅廓。請派生自一個抽象(接口或抽象類)鼻忠。
不要覆蓋基類中已經(jīng)實現(xiàn)的方法。如果覆蓋基類已經(jīng)實現(xiàn)的方法杈绸,那么你的基類就不是一個真正適合被繼承的抽象基類中已經(jīng)實現(xiàn)的方法粥烁,應(yīng)該有所有的子類共享贤笆。
如果說建一個披薩店,第一想到的是有很多披薩讨阻,從生產(chǎn)到包裝的流水線芥永,這樣就是從頂端開始想;讓我們反過來钝吮,從披薩開始想能抽象什么埋涧,這就是倒置。
--
如果各種披薩用不同的風(fēng)味的配料奇瘦,也可以用不同的配料工廠來實現(xiàn)棘催,例如:
在原料工廠的部分,引入了抽象工廠耳标,也就是會有一個factory抽象類醇坝,擁有create各種的方法,而各種特殊風(fēng)味調(diào)料的創(chuàng)建工廠實現(xiàn)這個抽象工廠次坡。
抽象工廠模式:提供一個接口呼猪,用于創(chuàng)建相關(guān)或依賴對象的家族,而不需要明確指定具體類砸琅。
抽象工廠比較神奇的是宋距,他的每個抽象方法的具體實現(xiàn)都是一個工廠的概念,例如createXXX症脂。抽象工廠會有很多這種create的方法谚赎,也就是會組織一群相關(guān)的產(chǎn)品的創(chuàng)建,而不止一種诱篷。
所以以上一共有三種工廠:
- 簡單工廠: 由一個類來創(chuàng)建對象
- 工廠方法:父類規(guī)定創(chuàng)建對象的create方法壶唤,子類繼承并實現(xiàn)各自的create方法
- 抽象工廠:抽象類定義一群create方法,再創(chuàng)建幾個工廠實現(xiàn)抽象工廠棕所,然后傳入工廠實例給其他用到的對象视粮,用組合的方式提供產(chǎn)品給需要的人