面向?qū)ο笤瓌t和OOD實(shí)際上是兩個(gè)不同的方面。
面向?qū)ο笤瓌t:封裝、繼承、多態(tài)。
OOP指的是面向?qū)ο缶幊痰幕驹瓌t和核心思路。在這里,OOP可以比作英語基礎(chǔ)語法,這些語法教你如何用單詞構(gòu)造有意義且正確的句子,OOP教你在代碼中構(gòu)造類,并在類里封裝屬性和方法,同時(shí)構(gòu)造他們之間的層次關(guān)系。
現(xiàn)在假定你需要就某些主題寫幾篇文章或隨筆。你也希望就幾個(gè)你擅長主題寫幾本書。對寫好文章/隨筆或書來說,知道如何造句是不夠的。為了使讀者能更輕松的明白你講的內(nèi)容,你需要寫更多的內(nèi)容,學(xué)習(xí)以更好的方式解釋它。如果你想就某個(gè)主題寫一本書,如學(xué)習(xí)OOD,你知道如何把一個(gè)主題分為幾個(gè)子主題。你需要為這些題目寫幾章內(nèi)容,也需要在這些章節(jié)中寫前言,簡介,例子和其他段落。 你需要為寫個(gè)整體框架,并學(xué)習(xí)一些很好的寫作技巧以便讀者能更容易明白你要說的內(nèi)容。這就是整體規(guī)劃。
在軟件開發(fā)中,OOD是整體思路。在某種程度上,設(shè)計(jì)軟件時(shí),你的類和代碼需能達(dá)到模塊化,可復(fù)用,且靈活,這些很不錯(cuò)的指導(dǎo)原則不用你重新發(fā)明創(chuàng)造。而且有些原則你在自己的代碼中可能用到了。
軟件開發(fā)唯一的真理是“軟件一定會變化”。為什么?因?yàn)槟愕能浖鉀Q的是現(xiàn)實(shí)生活中的業(yè)務(wù)問題,而現(xiàn)實(shí)生活中的業(yè)務(wù)流程總是在不停的變化。
假設(shè)你的軟件在今天工作的很好。但它能靈活的支持“變化”嗎?如果不能,那么你就沒有一個(gè)設(shè)計(jì)敏捷的軟件(一個(gè)設(shè)計(jì)敏捷的軟件能輕松應(yīng)對變化,能被擴(kuò)展,并且能被復(fù)用)。并且應(yīng)用好OOD是做到敏捷設(shè)計(jì)的關(guān)鍵。
1、面向?qū)ο?/p>
2、復(fù)用
3、能以最小的代價(jià)滿足變化
4、不用改變現(xiàn)有代碼滿足擴(kuò)展
最基本的是叫做SOLID的5原則(感謝Uncle Bob,偉大OOD導(dǎo)師)。
S = 單一職責(zé)原則 Single Responsibility PRinciple
O = 開放閉合原則 Opened Closed Principle
L = Liscov替換原則 Liscov Substitution Principle
I = 接口隔離原則 Interface Segregation Principle
D = 依賴倒置原則 Dependency Inversion Principle

單一職責(zé)原則海報(bào)
海報(bào)上說:"并不是因?yàn)槟隳埽憔蛻?yīng)該做"。為什么?因?yàn)殚L遠(yuǎn)來看它會帶來很多管理問題。
從面向?qū)ο蠼嵌冉忉尀椋?quot;引起類變化的因素永遠(yuǎn)不要多于一個(gè)。"或者說"一個(gè)類有且只有一個(gè)職責(zé)"。這個(gè)原則是說,如果你的類有多于一個(gè)原因會導(dǎo)致它變化(或者多于一個(gè)職責(zé)),你需要依據(jù)它們的職責(zé)把這個(gè)類拆分為多個(gè)類。
當(dāng)然可以在一個(gè)類中包含多個(gè)方法。問題是,他們都是為了一個(gè)目的。如今為什么拆分很重要?
那是因?yàn)椋?/p>
1、每個(gè)職責(zé)是軸向變化的;
2、如果類包含多個(gè)職責(zé),代碼會變得耦合;
看一下下面的類層次。

違反單一職責(zé)原則的類結(jié)構(gòu)圖
這里,Rectangle類做了下面兩件事:
1、計(jì)算矩形面積;
2、在界面上繪制矩形;
并且,有兩個(gè)應(yīng)用使用了Rectangle類:
1、計(jì)算幾何應(yīng)用程序用這個(gè)類計(jì)算面積;
2、圖形程序用這個(gè)類在界面上繪制矩形;
Rectangle類做了兩件事。在一個(gè)方法里它計(jì)算了面積,在另外一個(gè)方法了它返回一個(gè)表示矩形的GUI。這會帶來一些有趣的問題:
1.在計(jì)算幾何應(yīng)用程序中我們必須包含GUI。也就是在開發(fā)幾何應(yīng)用時(shí),我們必須引用GUI庫;
2.圖形應(yīng)用中Rectangle類的變化可能導(dǎo)致計(jì)算幾何應(yīng)用變化,編譯和測試,反之亦然;
應(yīng)該依據(jù)職責(zé)拆分這個(gè)類,拆分職責(zé)到兩個(gè)不同的類中,如:
Rectangle:這個(gè)類應(yīng)該定義Area()方法;
RectangleUI:這個(gè)類應(yīng)繼承Rectangle類,并定義Draw()方法。
SRP是把事物分離成分子部分,以便于能被復(fù)用和集中管理。我們也可以把SRP應(yīng)用到方法之中,應(yīng)當(dāng)分解你的方法,讓每個(gè)方法只做某一項(xiàng)工作。那樣允許你復(fù)用方法,并且一旦出現(xiàn)變化,你能購以修改最少的代碼滿足變化。

從面向?qū)ο笤O(shè)計(jì)角度看,它可以這么說:"軟件實(shí)體(類,模塊,函數(shù)等等)應(yīng)當(dāng)對擴(kuò)展開放,對修改閉合。"
通俗來講,它意味著你應(yīng)當(dāng)能在不修改類的前提下擴(kuò)展一個(gè)類的行為。就好像我不需要改變我的身體而可以穿上衣服。

客戶端和服務(wù)段都耦合在一起。那么,只要出現(xiàn)任何變化,服務(wù)端變化了,客戶端一樣需要改變。

在這個(gè)例子中,添加了一個(gè)抽象的服務(wù)器類,客戶端包含一個(gè)抽象類的引用,具體的服務(wù)類實(shí)現(xiàn)了抽象服務(wù)類。那么,因任何原因引起服務(wù)實(shí)現(xiàn)發(fā)生變化時(shí),客戶端都不需要任何改變。
這里抽象服務(wù)類對修改是閉合的,實(shí)體類的實(shí)現(xiàn)對擴(kuò)展是開放的。
抽象是關(guān)鍵,基本上,你抽象的東西是你系統(tǒng)的核心內(nèi)容,如果你抽象的好,很可能在擴(kuò)展功能時(shí)它不需要任何修改(就像服務(wù)是一個(gè)抽象概念)。如果在實(shí)現(xiàn)里定義了抽象的東西(比如IIS服務(wù)器實(shí)現(xiàn)的服務(wù)),代碼要盡可能以抽象(服務(wù))為依據(jù)。這會允許你擴(kuò)展抽象事物,定義一個(gè)新的實(shí)現(xiàn)(如Apache服務(wù)器)而不需要修改任何客戶端代碼。

這個(gè)原則意思是:"子類型必須能夠替換它們父類型。"或者換個(gè)說法:"使用父類引用的函數(shù)必須能使用子類的對象而不必知道它。"
在基本的面向?qū)ο笤瓌t里,"繼承"通常是"is a"的關(guān)系。如果"Developer" 是一個(gè)"SoftwareProfessional",那么"Developer"類應(yīng)當(dāng)繼承"SoftwareProfessional"類。在類設(shè)計(jì)中"Is a"關(guān)系非常重要,但它容易沖昏頭腦,結(jié)果使用錯(cuò)誤的繼承造成錯(cuò)誤設(shè)計(jì)。
"Liskov替換原則"正是保證繼承能夠被正確使用的方法。

這里,KingFisher類擴(kuò)展了Bird基類,并繼承了Fly()方法,這看起來沒問題。
現(xiàn)在看下面的例子:

Ostrich(鴕鳥)是一種鳥(顯然是),并從Bird類繼承。它能飛嗎?不能,這個(gè)設(shè)計(jì)就違反了LSP。
所以,即使在現(xiàn)實(shí)中看起來沒問題,在類設(shè)計(jì)中,Ostrich不應(yīng)該從Bird類繼承,這里應(yīng)該從Bird中分離一個(gè)不會飛的類,Ostrich應(yīng)該繼承與它。
如果沒有LSP,類繼承就會混亂;如果子類作為一個(gè)參數(shù)傳遞給方法,將會出現(xiàn)未知行為;
如果沒有LSP,適用與基類的單元測試將不能成功用于測試子類;

它的意思是:"客戶端不應(yīng)該被迫依賴于它們不用的接口。"
假設(shè)你想買個(gè)電視機(jī),你有兩個(gè)選擇。一個(gè)有很多開關(guān)和按鈕,它們看起來很混亂,且好像對你來說沒必要。另一個(gè)只有幾個(gè)開關(guān)和按鈕,它們很友好,且適合你使用。假定兩個(gè)電視機(jī)提供同樣的功能,你會選哪一個(gè)?答:第二個(gè),因?yàn)槲也恍枰切┛雌饋砘靵y又對我沒用的開關(guān)和按鈕。
以便外部能夠知道這些類有哪些可用的功能,客戶端代碼也能根據(jù)接口來設(shè)計(jì).現(xiàn)在,如果接口太大,包含很多暴露的方法,在外界看來會很混亂.接口包含太多的方法也使其可用性降低,像這種包含了無用方法的"胖接口"會增加類之間的耦合.你通過接口暴露類的功能,同樣地,假設(shè)你有一些類,如果一個(gè)類想實(shí)現(xiàn)該接口,那么它需要實(shí)現(xiàn)所有的方法,盡管有些對它來說可能完全沒用.所以說這么做會在系統(tǒng)中引入不必要的復(fù)雜度,降低可維護(hù)性或魯棒性.
接口隔離原則確保實(shí)現(xiàn)的接口有他們共同的職責(zé),它們是明確的,易理解的,可復(fù)用的.
接口應(yīng)該僅包含必要的方法,而不該包含其它的。

注意到IBird接口包含很多鳥類的行為,包括Fly()行為.現(xiàn)在如果一個(gè)Bird類(如Ostrich)實(shí)現(xiàn)了這個(gè)接口,那么它需要實(shí)現(xiàn)不必要的Fly()行為(Ostrich不會飛).
所以,這個(gè)"胖接口"應(yīng)該拆分未兩個(gè)不同的接口,IBird和IFlyingBird,IFlyingBird繼承自IBird.

這里如果一種鳥不會飛(如Ostrich),那它實(shí)現(xiàn)IBird接口。如果一種鳥會飛(如KingFisher),那么它實(shí)現(xiàn)IFlyingBird.
所以回頭看包含了很多開關(guān)和按鈕的電視機(jī)的例子,電視機(jī)制造商應(yīng)該有一個(gè)電視機(jī)的圖紙,開關(guān)和按鈕都在這個(gè)方案里。不論任何時(shí)候,當(dāng)他們向制造一種新款電視機(jī)時(shí),如果他們想復(fù)用這個(gè)圖紙,他們將需要在這個(gè)方案里添加更多的開關(guān)和按鈕。那么他們將沒法復(fù)用這個(gè)方案。如果他們確實(shí)需要復(fù)用方案,它們應(yīng)當(dāng)把電視機(jī)的圖紙份為更小部分,以便在任何需要造新款電視機(jī)的時(shí)候復(fù)用這點(diǎn)小部分。

它的意思是:高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象。
考慮一個(gè)現(xiàn)實(shí)中的例子。你的汽車是由很多如引擎,車輪,空調(diào)和其它等部件組成。
它們沒有一個(gè)是嚴(yán)格的構(gòu)建在一個(gè)單一單元里;換句話說,它們都是可插拔的,因此當(dāng)引擎或車輪出問題時(shí),你可以修理它(而不需要修理其它部件),甚至可以換一個(gè)。
在替換時(shí),你僅需要確保引擎或車輪符合汽車的設(shè)計(jì)(如汽車能使用任何1500CC的引擎或任何18寸的車輪)。
當(dāng)然,汽車也可能允許你在1500CC引擎的地方安裝一個(gè)2000CC的引擎,事實(shí)上對某些制造商(如豐田汽車)是一樣的。
如果你的汽車的零部件不具備可插拔性會有什么不同?那會很可怕!因?yàn)槿绻嚨囊娉龉收狭?,你可能修理整部車或者需要買一個(gè)新的。
如何做到"可插拔性"呢?關(guān)鍵就是抽象。
在現(xiàn)實(shí)中,汽車是高級模塊或?qū)嶓w,它依賴于低級模塊或?qū)嶓w,如引擎或車輪。相比直接依賴于引擎或車輪,汽車應(yīng)依賴于某些抽象的有規(guī)格的引擎或車輪,以便于如果任何引擎或車輪符合抽象,那么它們都能組合到汽車中,汽車也能跑動。

注意到上面Car類有兩個(gè)屬性,它們都是抽象類型(接口)。引擎和車輪是可插拔的,因?yàn)槠嚹芙邮苋魏螌?shí)現(xiàn)了聲明接口的對象,并且Car類不需要做任何改動。
如果代碼中不用依賴倒置,我們將面臨如下風(fēng)險(xiǎn):
1、使用低級類會破環(huán)高級代碼;
2、當(dāng)?shù)图夘愖兓瘯r(shí)需要很多時(shí)間和代價(jià)來修改高級代碼;
3、產(chǎn)生低復(fù)用的代碼;
除SOLID原則外還有很多其它的面向?qū)ο笤瓌t。如:
"組合替代繼承":這是說相對于繼承,要更傾向于使用組合;
"笛米特法則":這是說"你的類對其它類知道的越少越好";
"共同封閉原則":這是說"相關(guān)類應(yīng)該打包在一起";
"穩(wěn)定抽象原則":這是說"類越穩(wěn)定,越應(yīng)該由抽象類組成";
設(shè)計(jì)模式只是對一些經(jīng)常出現(xiàn)的場景的一些通用設(shè)計(jì)建議。這些靈感主要來自于面向?qū)ο笤瓌t。你可以把設(shè)計(jì)模式看作"框架",把OOD原則看作"規(guī)范".
新聞熱點(diǎn)
疑難解答