转:超越设计模
?
簡介:?可復用面向對象軟件的基礎 -- 設計模式,以其可復用的設計初衷、精巧的邏輯思維被廣大面向對象程序設計所追捧。但不少程序設計者卻經常將思考的問題轉換為遇到了什么場景就要用什么模式。這種八股文式的思維在某種程度上嚴重影響了程序設計的藝術性,并固化了程序設計者的思想,違背了設計模式的初衷。在本文中,作者總結了設計模式背后的核心思想,并提出了幾個關鍵的設計原則,例如面向接口、封裝變化、依賴倒置原則、只和朋友交談等。程序設計者只需在程序設計時遵循這些原則,便會發現原來已經在使用某些設計模式了。
引題
GOF 的設計模式推出以后,受到程序員的熱烈追捧,很多程序員不亦樂乎的埋頭苦讀甚至背誦其 23 個設計模式,并以熟悉設計模式而自豪。然而,在實際的程序設計中,很多程序員并未能把設計模式應用到自己的場景中。原因有很多,設計模式太多以至于常常被混淆;設計模式應用場景太局限或者程序員自己意識不到應用的場景。綜合各種原因,根本原因只有一個,程序員并不能透徹理解,熟練應用設計模式的核心思想。筆者認為,設計模式并不是條條框框,設計模式也不是簡單的 23 種。設計模式體現的一種思想是:盡可能的復用,而實現可復用的手段無外乎筆者總結的幾個設計原則而已。
徹底的忘掉 GOF 的設計模式吧,程序設計應該是一門藝術,而不是備受束縛的那些模式。
回頁首
原則一:封裝變化
該原則的核心思想是,在程序設計中找出應用中可能需要變化之處,把它們獨立出來以便以后可以輕易的改動或者擴充,而不影響不需要變化的部分。事實上如果您回過頭去重新閱讀設計模式的書籍,您會發現,封裝變化幾乎是每個設計模式背后的精神所在。所有的模式都提供了一套方法讓系統中的某部分改變不會影響其它部分。
我們舉一個簡單的例子,我們建立一個 Car 的基類,有兩個繼承類 Benz 和 BMW, 具體參見下圖 1:
圖 1. Car 的第一個實現
?
相信大部分人都會這么設計,但是這個設計有什么問題呢?我們看待問題需要以發展的眼光,假如科技發展了,所有的 Car 都可以飛了,怎么辦?有人說,很簡單,給 Car 加一個 protected 的 fly() 方法,這樣 Benz 和 BMW 就都可以飛了,繼承真偉大!好,那么如果我需要建立另外一個 Car 的子類玩具車(Toycar), 我們知道玩具車可以 run, 但不能 fly 的。怎么辦?還是好辦,我們可以重載玩具車的 fly 方法,讓他們什么都不干。那好,又一個子類來了,模型車(ModelCar)。模型車不能 run,不能 fly,好辦,繼續重載他們的這些方法。見下圖 2:
圖 2. Car 的第二個實現
?
如果我們有更多的 Car 的子類呢?有沒有覺得有點繁瑣,是的,我們需要重載太多的方法了。
繼承并不能幫我們解決問題!
可不可以使用接口,我們可以把 fly 從超類中取出來,分別作為接口,Flyable,這樣一來只有 Benz 和 BMW 才實現 Flyable 接口,ToyCar 和 modelCar 并不實現該接口。Run 也作類似處理。見下圖 3:
圖 3. Car 的第三個實現
?
大家可以看到,這其實是一個很笨的辦法,除去 description() 方法,我們使用繼承需要重載 3 個方法,可是我們使用接口實現則需要額外定義兩個接口類和5個方法。接口方法里面并不能有實現代碼,而 ToyCar 的 fly 行為和 Benz 的飛行行為也可能不盡相同,那么這就意味著我們需要實現越來越多的 fly() 方法。
接口也不行!
怎么辦?想一想我們的前面提到的設計原則,把變化的和不變化的分離開來,以便我們以后可以輕易的改動和擴充,而不影響其它不需要變化的部分。我們變化的部分是什么?是否可以飛行,是否可以 run,以何種方式飛行?何種方式 run ? Benz,BMW 和 ToyCar 的飛行行為和 run 行為各不相同。我們可以把這些不同的 fly 和 run 抽象出來。見如下圖 4:
圖 4. Car 的第四個實現
?
看到這,也許您應該大概明白接下來應該怎么辦了。是的,很簡單,我們可以給 Car 類加入飛行行為和 run 行為的實例變量。而在初始化 Car 的子類時傳入的具體行為進行初始化,這樣每個子類就自然擁有了相應的行為。
代碼參見如下:
清單 1. Car 的實現類
| 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(); } } |
?
上述代碼中我們實現了 Benz,如果我們要實現 ToyCar,一個不能飛,但可以跑。嘗試一下,看看多簡單。
清單 2. ToyCar 的實現類
| 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(); } } |
?
總結
策略模式定義
定義了算法族,并分別封裝起來,讓他們之間可以相互替換,此模式讓算法的變化獨立于使用算法的客戶。
回頁首
原則二:只和朋友交談
繼續我們的話題,假設我們有一家汽車公司,可以生產 Benz、BMW、ToyCar 和 ModelCar(姑且這么認為吧,雖然這不太符合常理),那么該如何設計我們的實現呢?很簡單,參見下面代碼:
清單 3. CarCompany 類
| 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; } } |
?
老問題,上面的代碼有問題么?但從業務邏輯上講,當然沒問題。可是還是要用變化的眼光看問題,上面的代碼維護起來成本很高。上面的代碼要求我們無論是 Benz、BMW、ToyCar 還是 ModelCar 都不能在將來發生變化。否則,我們這段代碼就有維護的成本和風險。
有沒有更有效的辦法,想想我們第一個設計原則:把變化的部分提出去,變化的部分是什么,顯然生成 car 的那一段。我們把它提出去,參見下面代碼:
清單 4. CarFactory 和 CarCompany 另一種實現
| 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 現在只依賴 CarFactory 一個類了。有人說這么做有什么用,我們只不過把問題轉移到另外一個對象 CarFactory 了,問題依然存在。但是別忘了,我們的 CarCompany 可能不止一個 produce() 方法。它可能還有 sale(), repair() 方法。這樣,我們相當于是把幾個問題縮小為一個問題了。
總結
回頁首
原則三:依賴倒置原則(DIP)
在您的設計里面,一定要減少對具體類的依賴,盡量依賴抽象,不要依賴具體類。這就是依賴倒置原則。
聽起來有點像面向接口,不針對實現編程。的確很類似,但是這里強調的是抽象。具體說來就是不要讓高層組件依賴低層組件,而且不管高層低層組件,都應該依賴抽象。高層組件最多是依賴低層組件的抽象。低層的抽象和實現也只依賴于高層的抽象。
所謂高層組件是由其它低層組件定義其行為的類。高層組件是包含重要的業務模型和策略選擇,低層模塊則是不同業務和策略實現。
也許您不是很理解這段話的含義。不要緊,繼續我們上面的例子。假設隨著汽車公司規模越來越大,業務規模拓展到了亞洲和歐洲。我們希望可以針對亞洲人和歐洲人生產出不同的同一品牌的的汽車。比如同一品牌 BMW 亞洲是左駕駛座(當然除了一些特殊地區),歐洲是右駕駛座。看看下面的實現。
清單 5. DependencyCarCompany 實現
| 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; } } } |
?
夠簡單吧!總結它們對象之間依賴的情況如圖 5 所示:
圖 5. CarCompany 的第一個實現
?
我們發現 CarComany 依賴的具體類有 8 個,如果任何一個類發生改變,CarCompany 都需要改變。這至少不符合我們的原則二:只和朋友交談。應用我原則一,把變化的部分提出來。我們可以定義兩個 CarCompany 的子類:AsiaCarCompany 和 EuropeCarCompany 用來生產不同樣式的同一品牌的汽車。在這兩個子類里面,需要做的就是生成不同品牌和樣式的汽車,然后再調用超類的三個方法。這樣的話我們可以把生成汽車的方法提出來。
清單 6. CarCompany 另一種實現
| 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 的問題在于,它依賴于每一個具體的 Car 類型。然而,在應用第二種方法后,CarCompany 現在只依賴 Car 類型的抽象,不再依賴具體類型,而是把這些依賴轉移到子類中。我們可以畫一個對象依賴圖 6:
圖 6. CarCompany 的第二個實現
?
從這個圖中我們可以看出:
總結
工廠方法模式定義
定義一個創建對象的接口,但由子類決定要實例化哪個類。工廠方法讓類的實例化推遲到的子類。
- 任何變量都不應該持有一個指向具體類的引用。
- 任何類都不應該從具體類派生,而是派生一個抽象類。
- 任何方法都不應該覆蓋它的任何基類中已經實現了的方法。
回頁首
原則四:類應該對擴展開放,對修改關閉
繼續剛才的例子,隨著汽車公司業務越來越大,為了滿足不同客戶的不同需求,對于任一品牌的汽車我們將有不同型號的配置。我們的配置包括氣囊(Balloon)、天窗(SkyLight)以及自動加熱座椅(HeatedSeats)等。每款汽車的價格為汽車自身價值加上配件的價格。設計如下圖 7:
圖 7. 第一個實現
?
Oh, My God! 這是什么?類爆炸?!好可怕的一件事。可以想象出來,這樣的設計將來的維護成本又多高。假如我想增加新的配件怎么辦,假如我想增加新的品牌又怎么辦?
其實我們可以用實例變量和繼承來重構上面的設計。見下圖 8:
圖 8. 第二個實現
?
我們在超類 Car 里面 cost() 方法計算各種配件的價格,然后在子類里面覆蓋 cost() 方法,但是會調用超類 cost 方法得到配件價格和,然后再加上子類汽車的基本價格。
清單 7. Car 的另一種實現
| 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();} } |
?
怎么樣?看起來好像天衣無縫的解決了我們的問題。然而有下面幾個問題需要考慮:
為什么看起來完美的設計,會有這么多解決不了的問題? 因為它違背我們的設計原則:類應該對擴展開放,對修改關閉。我們的目標是允許類容易擴展。在不修改現有代碼的基礎上,就可以搭配新的行為。這樣設計才可以接受新的功能來應對改變的需求。
該原則最典型的應用就是裝飾模式。讓我們以裝飾模式的思想重構我們上面的實現。
見圖 9:
圖 9. 裝飾過程
?
參見我們的實現代碼。
清單 8. Car 的另一種實現
| 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; } } |
?
下面看看我們的測試類。
清單 9. Car 裝飾者的測試類
| 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());}...} |
?
怎么樣?回過頭,想一想我們前面提出的那四個問題,是用這種設計方式是不是可以很好地解決呢?
總結
圖 10. Java I/O
?
回頁首
結篇
設計原則不是統一的,不同人對有不同的設計原則有不同的見解,設計原則也不限于上面所陳述的幾點。然后設計原則大的方向是統一的,那就是讓代碼盡可能的應對變化,盡可能的可復用。設計模式不是萬能的,沒有設計模式也不是不能的。然而在程序設計過程中遵循一些最基本的設計原則則是一個優秀的程序員所必需的,良好的設計原則的應用可以讓您設計的程序從容應對可能的改變,可以讓您的代碼變得優雅而富有藝術性。
?
參考資料
學習
- 參考?設計模式概述,了解設計模式基本內容。?
- 查看教程“Java 設計模式 101”,了解設計模式基本詞匯以及簡單使用設計模式。?
- 查看教程“Java 設計模式 201:超越四人組”,了解設計模式深層次的應用。?
- 查看系列文章“從 Java 類庫看設計模式”,了解 JDK 在設計模式中的應用。?
- 技術書店:瀏覽關于這些和其他技術主題的圖書。
- developerWorks Java 技術專區:數百篇關于 Java 編程各個方面的文章。
討論
- 加入?My developerWorks 中文社區。
關于作者
劉旭進,IBM 中國開發中心軟件工程師,對開源軟件、REST、Web Service、Open Search 有濃厚興趣和深入研究。目前在 Lotus Connections Fils Team 從事 REST Service 開發相關的工作。
轉載于:https://www.cnblogs.com/persist/p/3181240.html
總結
- 上一篇: Linux重定向详解
- 下一篇: 如何使用VIM的Help