【正文】
法一樣。但是,當(dāng)我們要向程序中增加木頭模型鴨子時(shí)該怎么辦?木頭鴨子不能叫也不能飛…這里在繼承體系中有一個(gè)新的類—DecoyDuck。注意,和RubberDuck一樣,它不能飛,也不能叫。想一想:利用繼承來(lái)提供Duck的行為,這會(huì)導(dǎo)致下列那些缺點(diǎn)?(多選)A. 代碼在多個(gè)子類中重復(fù)。D. 很難知道所有鴨子的全部行為。B.運(yùn)行時(shí)的行為不容易改變。E. 鴨子不能同時(shí)又飛又叫。C. 我們不能讓鴨子跳舞。,造成其他鴨子不想要的改變。利用接口如何?Joe認(rèn)識(shí)到繼承可能不是答案,因?yàn)樗麆倓偰玫絹?lái)自主管的備忘錄,希望以后每六個(gè)月更新產(chǎn)品(至于更新的方法,他們還沒(méi)想到)。Joe知道規(guī)格會(huì)常常改變,每當(dāng)有新的鴨子子類出現(xiàn),他就要被迫檢查并可能需要覆蓋fly()和quark()……這簡(jiǎn)直是無(wú)窮無(wú)盡的噩夢(mèng)。所以,他需要一個(gè)更清晰的方法,讓“某些(而不是全部)”鴨子類型可飛或可叫。Joe: 我可以將fly()方法從Duck超類中拿出來(lái),重做一個(gè)具有fly()方法的Flyable()接口。那樣只有能夠飛的鴨子類需要實(shí)現(xiàn)該接口并且會(huì)有一個(gè)fly()方法。同樣,我可以做一個(gè)Quackable類,因?yàn)椴皇撬械镍喿佣紩?huì)叫。新的設(shè)計(jì)類圖如圖3所示。你覺(jué)得這個(gè)設(shè)計(jì)如何? 由接口設(shè)計(jì)的類圖如果你是Joe,你要怎么辦?我們知道,并非“所有”的子類都具有飛行和呱呱叫的行為,所以繼承并不是適當(dāng)?shù)慕鉀Q方式。雖然Flyable與Quackable可以解決“一部分”問(wèn)題(不會(huì)再有會(huì)飛的橡皮鴨),但是卻造成代碼無(wú)法復(fù)用,這只能算是從一個(gè)噩夢(mèng)跳進(jìn)另一個(gè)噩夢(mèng)。甚至在會(huì)飛的鴨子中,飛行的動(dòng)作可能還有多種變化……此時(shí),你可能正期盼著設(shè)計(jì)模式能騎著白馬來(lái)解救你離開(kāi)苦難的那一天。但是,如果直接告訴你答案,這有什么樂(lè)趣?我們會(huì)用老方法找出一個(gè)解決之道:“采用良好的面向?qū)ο筌浖O(shè)計(jì)原則”。Joe:如果有一種軟件構(gòu)件方法,使我們需要改變時(shí),對(duì)現(xiàn)有的代碼有最少的影響該是多么理想呀。我們可以花很少的時(shí)間重構(gòu)代碼而花更多的時(shí)間做更酷的事情…軟件開(kāi)發(fā)的一個(gè)不變真理好吧!在軟件開(kāi)發(fā)上,有什么是你可以深信不疑的?不管你在何處工作,構(gòu)建些什么,用何種編程語(yǔ)言,在軟件開(kāi)發(fā)上,一直伴隨你的那個(gè)不變真理是什么?圖4. 軟件開(kāi)發(fā)的不變真理(用鏡子來(lái)看答案)不管當(dāng)初軟件設(shè)計(jì)得多好,一段時(shí)間之后,總是需要成長(zhǎng)與改變,否則軟件會(huì)“死亡”。想一想:導(dǎo)致改變的因素很多。找出你的應(yīng)用中需要改變代碼的原因,一一列出來(lái)。(我們寫(xiě)下了一些我們的原因,給你起個(gè)頭。)1).我們的顧客或用戶需要?jiǎng)e的東西,或者想要新功能。2).我的公司決定采用別的數(shù)據(jù)庫(kù)產(chǎn)品,又從另一家廠商買(mǎi)了數(shù)據(jù),這造成數(shù)據(jù)格式不兼容。唉!把問(wèn)題歸零……現(xiàn)在我們知道使用繼承并不能很好地解決問(wèn)題,因?yàn)轼喿拥男袨樵谧宇惱锊粩嗟馗淖?,并且讓所有的子類都有這些行為是不恰當(dāng)。Flyable與Quackable接口一開(kāi)始似乎還挺不錯(cuò),解決了問(wèn)題(只有會(huì)飛的鴨子才繼承Flyable),但是Java接口不具有實(shí)現(xiàn)代碼,所以繼承接口無(wú)法做到代碼的復(fù)用。這意味著:無(wú)論何時(shí)你需要修改某個(gè)行為,你必須得往下追蹤到每一個(gè)定義此行為的類中!幸運(yùn)的是有一個(gè)設(shè)計(jì)原則,恰好適用于此狀況。第一個(gè)設(shè)計(jì)原則是:找出應(yīng)用中可能需要變化之處,把它們獨(dú)立出來(lái),不要和那些不需要變化的代碼混在一起。把會(huì)變化的部分取出并“封裝”起來(lái),好讓其他部分不會(huì)受到影響。結(jié)果如何?代碼變化引起的不經(jīng)意后果變少,系統(tǒng)變得有彈性。換句話說(shuō),如果每次新的需求一來(lái),都會(huì)使某方面的代碼發(fā)生變化,那么你就可以確定,這部分的代碼需要被抽出來(lái),和其他穩(wěn)定的代碼有所區(qū)分。下面是這個(gè)原則的另一種思考方式:“把會(huì)變化的部分取出并封裝起來(lái),以便以后可以輕易地改動(dòng)或擴(kuò)充此部分,而不影響不需要變化的其他部分”。這樣的概念很簡(jiǎn)單,幾乎是每個(gè)設(shè)計(jì)模式背后的精神所在。所有的模式都提供了一套方法讓“系統(tǒng)中的某部分改變不會(huì)影響其他部分”。好,該是把鴨子的行為從Duck類中取出的時(shí)候了!分開(kāi)變化和不會(huì)變化的部分從哪里開(kāi)始呢?就我們目前所知,除了fly()和quack()的問(wèn)題之外,Duck類還算一切正常,似乎沒(méi)有特別需要經(jīng)常變化或修改的地方。所以,除了某些小改變之外,我們不打算對(duì)Duck類做太多處理?,F(xiàn)在,為了要分開(kāi)“變化和不會(huì)變化的部分”,我們準(zhǔn)備建立兩組類(完全遠(yuǎn)離Duck類),一個(gè)是“fly”相關(guān)的,一個(gè)是“quack”相關(guān)的,每一組類將實(shí)現(xiàn)各自的動(dòng)作。比方說(shuō),我們可能有一個(gè)類實(shí)現(xiàn)“呱呱叫”,另一個(gè)類實(shí)現(xiàn)“吱吱叫”,還有一個(gè)類實(shí)現(xiàn)“安靜”。我們知道Duck類內(nèi)的fly()和quack()會(huì)隨著鴨子的不同而改變。為了要把這兩個(gè)行為從Duck類中分開(kāi),我們將把它們從Duck類中取出來(lái),建立一組新類來(lái)代表每個(gè)行為。請(qǐng)看圖5。設(shè)計(jì)鴨子的行為如何設(shè)計(jì)那組實(shí)現(xiàn)飛行和呱呱叫的行為的類呢?我們希望一切都有彈性,畢竟,正是因?yàn)橐婚_(kāi)始鴨子行為沒(méi)有彈性,才讓我們走上現(xiàn)在這條路。我們還想能夠“指定”行為到鴨子的實(shí)例。比方說(shuō),我們想要產(chǎn)生一個(gè)新的綠頭鴨實(shí)例,并指定特定“類型”的飛行行為給它。干脆順便讓鴨子的行為可以動(dòng)態(tài)地改變好了。換句話說(shuō),我們應(yīng)該在鴨子類中包含設(shè)定行為的方法,這樣就可以在“運(yùn)行時(shí)”動(dòng)態(tài)地“改變”綠頭鴨的飛行行為。有了這些需要實(shí)現(xiàn)的目標(biāo),接著看看第二個(gè)設(shè)計(jì)原則:針對(duì)接口編程,而不是針對(duì)實(shí)現(xiàn)編程。從現(xiàn)在開(kāi)始,鴨子的行為將被放在分開(kāi)的類中,此類專門(mén)提供某行為接口的實(shí)現(xiàn)。這樣,鴨子類就不再需要知道行為的實(shí)現(xiàn)細(xì)節(jié)。圖5 將變化的部分從不變的部分中分出來(lái)我們利用接口代表每個(gè)行為,比方說(shuō),F(xiàn)lyBehavior與QuackBehavior,而行為的每個(gè)實(shí)現(xiàn)都將實(shí)現(xiàn)其中的一個(gè)接口。所以這次鴨子類不會(huì)負(fù)責(zé)實(shí)現(xiàn)Flying與Quacking接口,反而是由我們制造一組其他類專門(mén)實(shí)現(xiàn)FlyBehavior與QuackBehavior,這就稱為“行為”類。由行為類而不是Duck類來(lái)實(shí)現(xiàn)行為接口。這樣的做法迥異于以往,以前的做法是:行為來(lái)自Duck超類的具體實(shí)現(xiàn),或是繼承某個(gè)接口并由子類自行實(shí)現(xiàn)而來(lái)。這兩種做法都依賴于“實(shí)現(xiàn)”,我們被實(shí)現(xiàn)綁得死死的,沒(méi)辦法更改行為(除非寫(xiě)更多代碼)。在我們的新設(shè)計(jì)中,鴨子的子類將使用接口(FlyBehavior 與QuakBehavior)所表示的行為,所以實(shí)際的“實(shí)現(xiàn)”不會(huì)被綁死在鴨子的子類中。(換句話圖6 Flybehavior 接口 說(shuō),特定的具體行為編寫(xiě)在實(shí)現(xiàn)了FlyBehavior與QuackBehavior的類中)。Joe:我不明白為什么你要用一個(gè)接口實(shí)現(xiàn)FlyBehavior??梢杂贸橄箢愖鐾瑯拥氖虑?。難道僅僅是想使用多態(tài)性嗎?“針對(duì)接口編程”真正的意思是“針對(duì)超類型(supertype)編程”。這里所謂的“接口”有多個(gè)含義,接口是一個(gè)“概念”,也是一種Java的interface構(gòu)造。你可以在不涉及Java interface的情況下,“針對(duì)接口編程”,關(guān)鍵就在多態(tài)。利用多態(tài),程序可以針對(duì)超類型編程,執(zhí)行時(shí)會(huì)根據(jù)實(shí)際狀況執(zhí)行真正的行為,不會(huì)被綁死在超類型的行為上?!搬槍?duì)超類型編程”這句話,可以更明確地說(shuō)成 “變量的聲明類型應(yīng)該是超類型,通常是一個(gè)抽象類或者是一個(gè)接口,如此,只要是具體實(shí)現(xiàn)此超類型的類所產(chǎn)生的對(duì)象,都可以指定給這個(gè)變量。這也意味著,聲明類時(shí)不用理會(huì)以后執(zhí)行時(shí)的真正對(duì)象類型!”這可能不是你第一次聽(tīng)到,但是請(qǐng)務(wù)必注意我們說(shuō)的是同一件事??纯聪旅孢@個(gè)簡(jiǎn)單的多態(tài)例子:假設(shè)有一個(gè)抽象類Animal,有兩個(gè)具體的實(shí)現(xiàn)(Dog與Cat)繼承Animal。做法如下:“針對(duì)實(shí)現(xiàn)編程”Dog d=new Dog()。 //聲明變量”d”為Dog類型(是Animal的具體實(shí)現(xiàn)),會(huì)造成我們必須()。 //針對(duì)具體實(shí)現(xiàn)編碼。但是,“針對(duì)接口/超類型編程”做法會(huì)如下:Animal animal=new Dog()。 //我們知道該對(duì)象是狗,但是我們現(xiàn)在利用animal進(jìn)行多態(tài)的()。 //調(diào)用。更棒的是,子類實(shí)例化的動(dòng)作不再需要在代碼中硬編碼,例如new Dog(),而是“在運(yùn)行時(shí)才指定具體實(shí)現(xiàn)的對(duì)象”。 a=getAnimal()。 //我們不知道實(shí)際的子類型是“什么”……我們只關(guān)心它知道如何正確進(jìn)行a. makeSound()。 //makeSound()的動(dòng)作就夠了。 圖7 針對(duì)超類型編程 圖8 實(shí)現(xiàn)鴨子的行為T(mén)ext B: 設(shè)計(jì)模式簡(jiǎn)介 2實(shí)現(xiàn)鴨子的行為在此,我們有兩個(gè)接口,F(xiàn)lyBehavior和QuackBehavior,還有它們對(duì)應(yīng)的類,負(fù)責(zé)實(shí)現(xiàn)具體的行為如圖8所示。這樣的設(shè)計(jì),可以讓飛行和呱呱叫的動(dòng)作被其他的對(duì)象復(fù)用,因?yàn)檫@些行為已經(jīng)與鴨子類無(wú)關(guān)了。而我們可以新增一些行為,不會(huì)影響到既有的行為類,也不會(huì)影響“使用”到飛行行為的鴨子類。這么一來(lái),有了繼承的“復(fù)用”的好處,卻沒(méi)有繼承所帶來(lái)的包袱。大拇指問(wèn)題:?jiǎn)枺何沂遣皇且欢ㄒ劝严到y(tǒng)做出來(lái),再看看有那些地方需要變化,然后才回頭去把這些地方分離并且封裝?答:不盡然。通常在你設(shè)計(jì)系統(tǒng)時(shí),預(yù)先考慮到有那些地方未來(lái)有可能需要變化,于是提前在代碼中加入這些彈性。你會(huì)發(fā)現(xiàn),原則與模式可以應(yīng)用在軟件開(kāi)發(fā)生命周期的任何階段。問(wèn):Duck是不是也該設(shè)計(jì)成一個(gè)接口?答:在本例中,這么做并不恰當(dāng)。如你所見(jiàn)的,我們已經(jīng)讓一切都整合妥當(dāng),而且讓Duck成為一個(gè)具體類,這樣可以讓衍生的特定類(例如綠頭鴨)具有Duck共同的屬性和方法。我們已經(jīng)從Duck的繼承結(jié)構(gòu)中刪除了變化的部分,原先的問(wèn)題都已經(jīng)解決了,所以不需要把Duck設(shè)計(jì)成接口。問(wèn):用一個(gè)類代表一個(gè)行為,感覺(jué)似乎有點(diǎn)奇怪。類不是應(yīng)該代表某種“東西”嗎?類不是應(yīng)該同時(shí)具備狀態(tài)“與”行為嗎?答:在面向?qū)ο笙到y(tǒng)中,是的,類代表的東西一般都是既有狀態(tài)(實(shí)例變量)又有方法。只是在本例中,碰巧“東西”是個(gè)行為。但是即使是行為,也仍然可以有狀態(tài)和方法。例如,飛行的行為可以具有實(shí)例變量,記錄飛行行為的屬性(每秒翅膀拍動(dòng)幾下、最大高度和速度等)。整合鴨子的行為關(guān)鍵在于,鴨子現(xiàn)在會(huì)將飛行和呱呱叫的動(dòng)作“委托”(delegate)另一個(gè)類處理,而不是使用定義在Duck類(或子類)內(nèi)的呱呱叫的飛行方法。做法是這樣的:圖9 整合鴨子的行為1) 首先,在Duck類中,“加入兩個(gè)實(shí)例變量”,分別為”flyBehavior”與”quackBehavior”,聲明為接口類型(而不是具體類實(shí)現(xiàn)類型),每個(gè)鴨子對(duì)象都會(huì)動(dòng)態(tài)地設(shè)置這些變量以在運(yùn)行時(shí)引用正確的行為類型(例如:FlyWithWings、Squeak等)。我們也必須將Duck類與其所有子類中的fly()與quack()刪除,因?yàn)檫@些行為已經(jīng)被搬到FlyBehavior與QuackBehavior類中了。我們用兩個(gè)相似的方法perforFly()和performQuack()取代Duck類中的fly()與quack()。稍后你就會(huì)知道為什么。2) 現(xiàn)在,我們來(lái)實(shí)現(xiàn)performQuack()。Public class Duck{QuackBehavior quackBehavior。 //Each Duck has a reference to something that //more //implement the QuackBehavior interfacePublic void performQuack(){ //Rather than handling the quack behavior itself, ()。 //the Duck object delegates that behavior to the} // object referenced by quackBehavior.}很容易,是吧?想進(jìn)行呱呱叫的動(dòng)作,Duck對(duì)象只要叫quackBehavior對(duì)象去呱呱叫就可以了。在這部分的代碼中,我們不在乎quackBehavior接口的對(duì)象到底是什么,我們只關(guān)心該對(duì)象知道如何進(jìn)行呱呱叫就夠了。3) 好吧!現(xiàn)在來(lái)關(guān)心“如何設(shè)定flyBehavior與quackBehavior”的實(shí)例變量。看看MallardDuck類:Public class MallardDuck extends Duck{Public MallardDuck(){ //A Mallard Duck uses the Quack class to handle quackBehavior=new Quack()。 //its quack, so when performQuack is called, theflyBehavior=new FlyWithWings()。//responsibility for the quack is delegated to the} //Quack object and we get a real quack //And it uses FlywithWings as its FlyBehavior type//Remember, MallardDuck inherits the quackBehavior and flyBehavior instance variables from class Duck.Public void display(){ (“I’m a real Mallard duck”)。}}所以,綠頭鴨會(huì)真的“呱呱叫”,而不是“吱吱叫”,或“叫不出聲”。這是怎么辦到的?當(dāng)MallardDuck實(shí)例化時(shí),它的構(gòu)造器會(huì)把繼承來(lái)的quackBehavior實(shí)例變量初始化成Quack類型的新實(shí)例(Quack是QuackBehavi