深入探討超越設(shè)計(jì)模式之外的設(shè)計(jì)原則
簡介: 可復(fù)用面向?qū)ο筌浖幕A(chǔ) -- 設(shè)計(jì)模式,以其可復(fù)用的設(shè)計(jì)初衷、精巧的邏輯思維被廣大面向?qū)ο蟪绦蛟O(shè)計(jì)所追捧。但不少程序設(shè)計(jì)者卻經(jīng)常將思考的問題轉(zhuǎn)換為遇到了什么場景就要用什么模式。這種八股文式的思維在某種程度上嚴(yán)重影響了程序設(shè)計(jì)的藝術(shù)性,并固化了程序設(shè)計(jì)者的思想,違背了設(shè)計(jì)模式的初衷。在本文中,作者總結(jié)了設(shè)計(jì)模式背后的核心思想,并提出了幾個(gè)關(guān)鍵的設(shè)計(jì)原則,例如面向接口、封裝變化、依賴倒置原則、只和朋友交談等。程序設(shè)計(jì)者只需在程序設(shè)計(jì)時(shí)遵循這些原則,便會發(fā)現(xiàn)原來已經(jīng)在使用某些設(shè)計(jì)模式了。
發(fā)布日期: 2010 年 10 月 21 日
級別: 中級
訪問情況 647 次瀏覽
建議: 5 (查看或添加評論)
GOF 的設(shè)計(jì)模式推出以后,受到程序員的熱烈追捧,很多程序員不亦樂乎的埋頭苦讀甚至背誦其 23 個(gè)設(shè)計(jì)模式,并以熟悉設(shè)計(jì)模式而自豪。然而,在實(shí)際的程序設(shè)計(jì)中,很多程序員并未能把設(shè)計(jì)模式應(yīng)用到自己的場景中。原因有很多,設(shè)計(jì)模式太多以至于常常被混淆;設(shè)計(jì)模式應(yīng)用場景太局限或者程序員自己意識不到應(yīng)用的場景。綜合各種原因,根本原因只有一個(gè),程序員并不能透徹理解,熟練應(yīng)用設(shè)計(jì)模式的核心思想。筆者認(rèn)為,設(shè)計(jì)模式并不是條條框框,設(shè)計(jì)模式也不是簡單的 23 種。設(shè)計(jì)模式體現(xiàn)的一種思想是:盡可能的復(fù)用,而實(shí)現(xiàn)可復(fù)用的手段無外乎筆者總結(jié)的幾個(gè)設(shè)計(jì)原則而已。
徹底的忘掉 GOF 的設(shè)計(jì)模式吧,程序設(shè)計(jì)應(yīng)該是一門藝術(shù),而不是備受束縛的那些模式。
該原則的核心思想是,在程序設(shè)計(jì)中找出應(yīng)用中可能需要變化之處,把它們獨(dú)立出來以便以后可以輕易的改動或者擴(kuò)充,而不影響不需要變化的部分。事實(shí)上如果您回過頭去重新閱讀設(shè)計(jì)模式的書籍,您會發(fā)現(xiàn),封裝變化幾乎是每個(gè)設(shè)計(jì)模式背后的精神所在。所有的模式都提供了一套方法讓系統(tǒng)中的某部分改變不會影響其它部分。
我們舉一個(gè)簡單的例子,我們建立一個(gè) Car 的基類,有兩個(gè)繼承類 Benz 和 BMW, 具體參見下圖 1:
相信大部分人都會這么設(shè)計(jì),但是這個(gè)設(shè)計(jì)有什么問題呢?我們看待問題需要以發(fā)展的眼光,假如科技發(fā)展了,所有的 Car 都可以飛了,怎么辦?有人說,很簡單,給 Car 加一個(gè) protected 的 fly() 方法,這樣 Benz 和 BMW 就都可以飛了,繼承真?zhèn)ゴ?!好,那么如果我需要建立另外一個(gè) Car 的子類玩具車(Toycar), 我們知道玩具車可以 run, 但不能 fly 的。怎么辦?還是好辦,我們可以重載玩具車的 fly 方法,讓他們什么都不干。那好,又一個(gè)子類來了,模型車(ModelCar)。模型車不能 run,不能 fly,好辦,繼續(xù)重載他們的這些方法。見下圖 2:
如果我們有更多的 Car 的子類呢?有沒有覺得有點(diǎn)繁瑣,是的,我們需要重載太多的方法了。
繼承并不能幫我們解決問題!
可不可以使用接口,我們可以把 fly 從超類中取出來,分別作為接口,F(xiàn)lyable,這樣一來只有 Benz 和 BMW 才實(shí)現(xiàn) Flyable 接口,ToyCar 和 modelCar 并不實(shí)現(xiàn)該接口。Run 也作類似處理。見下圖 3:
大家可以看到,這其實(shí)是一個(gè)很笨的辦法,除去 description() 方法,我們使用繼承需要重載 3 個(gè)方法,可是我們使用接口實(shí)現(xiàn)則需要額外定義兩個(gè)接口類和5個(gè)方法。接口方法里面并不能有實(shí)現(xiàn)代碼,而 ToyCar 的 fly 行為和 Benz 的飛行行為也可能不盡相同,那么這就意味著我們需要實(shí)現(xiàn)越來越多的 fly() 方法。
接口也不行!
怎么辦?想一想我們的前面提到的設(shè)計(jì)原則,把變化的和不變化的分離開來,以便我們以后可以輕易的改動和擴(kuò)充,而不影響其它不需要變化的部分。我們變化的部分是什么?是否可以飛行,是否可以 run,以何種方式飛行?何種方式 run ? Benz,BMW 和 ToyCar 的飛行行為和 run 行為各不相同。我們可以把這些不同的 fly 和 run 抽象出來。見如下圖 4:
看到這,也許您應(yīng)該大概明白接下來應(yīng)該怎么辦了。是的,很簡單,我們可以給 Car 類加入飛行行為和 run 行為的實(shí)例變量。而在初始化 Car 的子類時(shí)傳入的具體行為進(jìn)行初始化,這樣每個(gè)子類就自然擁有了相應(yīng)的行為。
代碼參見如下:
public abstract class Car { protected RunBehavior runBehavior; protected FlyBehaviro flyBehavior; public abstract description (); protected performFly() { flyBehavior.fly(); } protected performRun() { runBehavior.run(); } } public class Benz extends Car { public String description() { System.out.println(“I am Benz!”); } public Benz() { this.runBehavior = new HighSpeedRunBehavior(); this.flyBehavior = new HighFlyingBehavior(); } } |
上述代碼中我們實(shí)現(xiàn)了 Benz,如果我們要實(shí)現(xiàn) ToyCar,一個(gè)不能飛,但可以跑。嘗試一下,看看多簡單。
public class ToyCar extends Car { public String description() { System.out.println(“I am Toy car!”); } public Benz() { this.runBehavior = new LowSpeedRunBehavior(); this.flyBehavior = new NoFlyingBehavior(); } } |
繼續(xù)我們的話題,假設(shè)我們有一家汽車公司,可以生產(chǎn) Benz、BMW、ToyCar 和 ModelCar(姑且這么認(rèn)為吧,雖然這不太符合常理),那么該如何設(shè)計(jì)我們的實(shí)現(xiàn)呢?很簡單,參見下面代碼:
public class CarCompany { public CarCompany () { } public Car produce(String type) { Car car = null; if(“Benz”.equals(type)) { car = new Benz(); } else if(“BMW”.equals(type)) { car = new BMW(); } else if(“ToyCar”.equals(type)) { car = new ToyCar(); } else if(“ModelCar”.equals(type)) { car = new ModelCar(); } else { } car.assembly(); // 組裝 car.sprayPainting(); // 噴漆 car.proof(); // 校對 return car; } } |
老問題,上面的代碼有問題么?但從業(yè)務(wù)邏輯上講,當(dāng)然沒問題??墒沁€是要用變化的眼光看問題,上面的代碼維護(hù)起來成本很高。上面的代碼要求我們無論是 Benz、BMW、ToyCar 還是 ModelCar 都不能在將來發(fā)生變化。否則,我們這段代碼就有維護(hù)的成本和風(fēng)險(xiǎn)。
有沒有更有效的辦法,想想我們第一個(gè)設(shè)計(jì)原則:把變化的部分提出去,變化的部分是什么,顯然生成 car 的那一段。我們把它提出去,參見下面代碼:
public class CarFactory { public CarFactory () { } public Car createCar(String type) { Car car = null; if(“Benz”.equals(type)) { car = new Benz(); } else if(“BMW”.equals(type)) { car = new BMW(); } else if(“ToyCar”.equals(type)) { car = new ToyCar(); } else if(“ModelCar”.equals(type)) { car = new ModelCar(); } else { } return car; } } public class CarCompany { CarFactory carFactory; public CarCompany (CarFactory carFactory) { this.carFactory = carFactory; } public Car produce(String type) { Car car = null; Car = carFactory.createCar(type); car.assembly(); // 組裝 car.sprayPainting(); // 噴漆 car.proof(); // 校對 return car; } } |
很顯然,我們的 CarCompany 現(xiàn)在只依賴 CarFactory 一個(gè)類了。有人說這么做有什么用,我們只不過把問題轉(zhuǎn)移到另外一個(gè)對象 CarFactory 了,問題依然存在。但是別忘了,我們的 CarCompany 可能不止一個(gè) produce() 方法。它可能還有 sale(), repair() 方法。這樣,我們相當(dāng)于是把幾個(gè)問題縮小為一個(gè)問題了。
在您的設(shè)計(jì)里面,一定要減少對具體類的依賴,盡量依賴抽象,不要依賴具體類。這就是依賴倒置原則。
聽起來有點(diǎn)像面向接口,不針對實(shí)現(xiàn)編程。的確很類似,但是這里強(qiáng)調(diào)的是抽象。具體說來就是不要讓高層組件依賴低層組件,而且不管高層低層組件,都應(yīng)該依賴抽象。高層組件最多是依賴低層組件的抽象。低層的抽象和實(shí)現(xiàn)也只依賴于高層的抽象。
所謂高層組件是由其它低層組件定義其行為的類。高層組件是包含重要的業(yè)務(wù)模型和策略選擇,低層模塊則是不同業(yè)務(wù)和策略實(shí)現(xiàn)。
也許您不是很理解這段話的含義。不要緊,繼續(xù)我們上面的例子。假設(shè)隨著汽車公司規(guī)模越來越大,業(yè)務(wù)規(guī)模拓展到了亞洲和歐洲。我們希望可以針對亞洲人和歐洲人生產(chǎn)出不同的同一品牌的的汽車。比如同一品牌 BMW 亞洲是左駕駛座(當(dāng)然除了一些特殊地區(qū)),歐洲是右駕駛座??纯聪旅娴膶?shí)現(xiàn)。
public class DependencyCarCompany { public CarCompany () { } public Car produce(String style, String type) { Car car = null; if(“Asia”.equals(style)) { if(“Benz”.equals(type)) { car = new AsiaBenz(); } else if(“BMW”.equals(type)) { car = new AsiaBMW(); } else if(“ToyCar”.equals(type)) { car = new AsiaToyCar(); } else if(“ModelCar”.equals(type)) { car = new AsiaModelCar(); } else { } } else if(“Europe”.equals(style)) { if(“Benz”.equals(type)) { car = new EuropeBenz(); } else if(“BMW”.equals(type)) { car = new EuropeBMW(); } else if(“ToyCar”.equals(type)) { car = new EuropeToyCar(); } else if(“ModelCar”.equals(type)) { car = new EuropeModelCar(); } else { } car.assembly(); // 組裝 car.sprayPainting(); // 噴漆 car.proof(); // 校對 return car; } } } |
夠簡單吧!總結(jié)它們對象之間依賴的情況如圖 5 所示:
我們發(fā)現(xiàn) CarComany 依賴的具體類有 8 個(gè),如果任何一個(gè)類發(fā)生改變,CarCompany 都需要改變。這至少不符合我們的原則二:只和朋友交談。應(yīng)用我原則一,把變化的部分提出來。我們可以定義兩個(gè) CarCompany 的子類:AsiaCarCompany 和 EuropeCarCompany 用來生產(chǎn)不同樣式的同一品牌的汽車。在這兩個(gè)子類里面,需要做的就是生成不同品牌和樣式的汽車,然后再調(diào)用超類的三個(gè)方法。這樣的話我們可以把生成汽車的方法提出來。
public abstract class CarCompany { public Car produce(String type) { Car car = createCar(type); car.assembly(); // 組裝 car.sprayPainting(); // 噴漆 car.proof(); // 校對 return car; } protected abstract Car createCar(String type); } public class AsiaCarCompany { protected Car createCar(Sting type) { Car car = null; if(“Benz”.equals(type)) { car = new AsiaBenz(); } else if(“BMW”.equals(type)) { car = new AsiaBMW(); } else if(“ToyCar”.equals(type)) { car = new AsiaToyCar(); } else if(“ModelCar”.equals(type)) { car = new AsiaModelCar(); } else { } return car; } } public class EuropeCarCompany { protected Car createCar(Sting type) { Car car = null; if(“Benz”.equals(type)) { car = new EuropeBenz(); } else if(“BMW”.equals(type)) { car = new EuropeBMW(); } else if(“ToyCar”.equals(type)) { car = new EuropeToyCar(); } else if(“ModelCar”.equals(type)) { car = new EuropeModelCar(); } else { } return car; } } |
DependencyCarCompany 的問題在于,它依賴于每一個(gè)具體的 Car 類型。然而,在應(yīng)用第二種方法后,CarCompany 現(xiàn)在只依賴 Car 類型的抽象,不再依賴具體類型,而是把這些依賴轉(zhuǎn)移到子類中。我們可以畫一個(gè)對象依賴圖 6:
從這個(gè)圖中我們可以看出:
原則四:類應(yīng)該對擴(kuò)展開放,對修改關(guān)閉
繼續(xù)剛才的例子,隨著汽車公司業(yè)務(wù)越來越大,為了滿足不同客戶的不同需求,對于任一品牌的汽車我們將有不同型號的配置。我們的配置包括氣囊(Balloon)、天窗(SkyLight)以及自動加熱座椅(HeatedSeats)等。每款汽車的價(jià)格為汽車自身價(jià)值加上配件的價(jià)格。設(shè)計(jì)如下圖 7:
Oh, My God! 這是什么?類爆炸?!好可怕的一件事。可以想象出來,這樣的設(shè)計(jì)將來的維護(hù)成本又多高。假如我想增加新的配件怎么辦,假如我想增加新的品牌又怎么辦?
其實(shí)我們可以用實(shí)例變量和繼承來重構(gòu)上面的設(shè)計(jì)。見下圖 8:
我們在超類 Car 里面 cost() 方法計(jì)算各種配件的價(jià)格,然后在子類里面覆蓋 cost() 方法,但是會調(diào)用超類 cost 方法得到配件價(jià)格和,然后再加上子類汽車的基本價(jià)格。
public abstract class Car { protected Balloon balloon; protected SkyLight skyLight; protected HeatedSeat heatedSeat; protected int cost() { int res = 0; if(hasBalloon) { res += 25000; } if(hasSkyLight) { res += 20000; } if(hasHeatedSeat) { res +=10000; } } void setBalloon(Balloon balloon); boolean hasBalloon(); …. …. } public Benz extends Car { public Benz(Balloon blloon) { this.setBalloon(balloon); } public int cost() { int res= 1000000; res += super.cost(); } } |
怎么樣?看起來好像天衣無縫的解決了我們的問題。然而有下面幾個(gè)問題需要考慮:
為什么看起來完美的設(shè)計(jì),會有這么多解決不了的問題? 因?yàn)樗`背我們的設(shè)計(jì)原則:類應(yīng)該對擴(kuò)展開放,對修改關(guān)閉。我們的目標(biāo)是允許類容易擴(kuò)展。在不修改現(xiàn)有代碼的基礎(chǔ)上,就可以搭配新的行為。這樣設(shè)計(jì)才可以接受新的功能來應(yīng)對改變的需求。
該原則最典型的應(yīng)用就是裝飾模式。讓我們以裝飾模式的思想重構(gòu)我們上面的實(shí)現(xiàn)。
見圖 9:
參見我們的實(shí)現(xiàn)代碼。
public abstract class Car { protected abstract int cost(); } public class Benz extends Car { public int cost() { return 100000; } } public abstract class CarDecorator extends Car { protected abstract int cost(); } public class Balloon extends CarDecorator { public Car car; public Balloon(Car car) { this.car = car; } public int cost() { return car.cost() + 25000; } } public class SkyLight extends CarDecorator { public Car car; public SkyLight (Car car) { this.car = car; } public int cost() { return car.cost() + 20000; } } public class HeatedSeat extends CarDecorator { public Car car; public HeatedSeat (Car car) { this.car = car; } public int cost() { return car.cost(0 + 10000; } } |
下面看看我們的測試類。
public class CarWithDecorator { public static void mian(String[] args) { Car car = new BMW(); car = new Balloon(car); car = new SkyLight(car); car = new HeatedSeat(car); car = new Balloon(car); System.out.println(car.cost()); } ... } |
怎么樣?回過頭,想一想我們前面提出的那四個(gè)問題,是用這種設(shè)計(jì)方式是不是可以很好地解決呢?
設(shè)計(jì)原則不是統(tǒng)一的,不同人對有不同的設(shè)計(jì)原則有不同的見解,設(shè)計(jì)原則也不限于上面所陳述的幾點(diǎn)。然后設(shè)計(jì)原則大的方向是統(tǒng)一的,那就是讓代碼盡可能的應(yīng)對變化,盡可能的可復(fù)用。設(shè)計(jì)模式不是萬能的,沒有設(shè)計(jì)模式也不是不能的。然而在程序設(shè)計(jì)過程中遵循一些最基本的設(shè)計(jì)原則則是一個(gè)優(yōu)秀的程序員所必需的,良好的設(shè)計(jì)原則的應(yīng)用可以讓您設(shè)計(jì)的程序從容應(yīng)對可能的改變,可以讓您的代碼變得優(yōu)雅而富有藝術(shù)性。
學(xué)習(xí)
討論