此文譯自CodeProject上一文,該文章在Top Articles上排名第3,讀了以後以爲很是好,就翻譯出來,供不想讀英文的同窗參考學習。做者(Shubho)的妻子(Farhana)打算從新作一 名軟件工程師(她原本是,後來由於他們孩子出生放棄了),因而做者就試圖根據本身在軟件開發設計方面的經驗幫助她學習面向對象設計(OOD)。編程
自做者從事軟件開發開始,做者經常注意到無論技術問題看起來多複雜,若是從現實生活的角度解釋並以對答的方式討論,那麼它將變得更簡單。如今他們把在OOD方面有些富有成效的對話分享出來,你可能會發現那是一種學習OOD頗有意思的方式。設計模式
下面就是他們的對話:瀏覽器
OOD簡介服務器
Shubho:親愛的,讓咱們開始學習OOD吧。你瞭解面向對象原則嗎?併發
Farhana:你是說封裝,繼承,多態對嗎?我知道的。框架
Shubho:好,我但願你已瞭解如何使用類和對象。今天咱們學習OOD。ide
Farhana:等一下。面向對象原則對面向對象編程(OOP)來講不夠嗎?個人意思是我會定義類,並封裝屬性和方法。我也能根據類的關係定義它們之間的層次。若是是,那麼還有什麼?模塊化
Shubho:問得好。面向對象原則和OOD其實是兩個不一樣的方面。讓我給你舉個實際生活中的例子幫你弄明白。函數
再你小時候你首先學會字母表,對嗎?單元測試
Farhana:嗯
Shubho:好。你也學了單詞,並學會如何根據字母表造詞。後來你學會了一些造句的語法。例如時態,介詞,連詞和其餘一些讓你能造出語法正確的句子。例如:
"I" (代詞) "want" (動詞) "to" (介詞) "learn" (動詞) "OOD"(名詞)。
看,你按照某些規則組合了單詞,而且你選擇了有某些意義的正確的單詞結束了句子。
Farhana:OK,這意味着什麼呢?
Shubho:面向對象原則與這相似。OOP指的是面向對象編程的基本原則和核心思路。在這裏,OOP能夠比做英語基礎語法,這些語法教你如何用單詞構造有意義且正確的句子,OOP教你在代 碼中構造類,並在類裏封裝屬性和方法,同時構造他們之間的層次關係。
Farhana:嗯..我有點感受了,這裏有OOD嗎?
Shubho:立刻就有答案。如今假定你須要就某些主題寫幾篇文章或隨筆。你也但願就幾個你擅長主體寫幾本書。對寫好文章/隨筆或書來講,知道如何造句是不夠的,對嗎?爲了使讀者能更輕 鬆的明白你講的內容,你須要寫更多的內容,學習以更好的方式解釋它。
Farhana:看起來有點意思...繼續。
Shubho:如今,若是你想就某個主題寫一本書,如學習OOD,你知道如何把一個主題分爲幾個子主題。你須要 爲這些題目寫幾章內容,也須要在這些章節中寫前言,簡介,例子和其餘段落。 你須要爲寫個總體框架,並學習一些很好的寫做技巧以便讀者能更容易明白你要說的內容。這就是總體規劃。
在軟件開發中,OOD是總體思路。在某種程度上,設計軟件時,你的類和代碼需能達到模塊化,可複用,且靈活,這些很不錯的指導原則不用你從新發明創造。確實有些原則你已經在你的類和對象中已經用到了,對嗎?
Farhana:嗯...有個大概的印象了,但須要繼續深刻。
Shubho:別擔憂,你立刻就會學到。咱們繼續討論下去。
爲何要OOD?
Shubho:這是一個很是重要的問題。當咱們能很快地設計一些類,完成開發併發布時,爲何咱們須要關心OOD?那樣子還不夠嗎?
Farhana:嗯,我早先並不知道OOD,我一直就是開發併發布項目。那麼關鍵是什麼?
Shubho:好的,我先給你一句名言:
走在結冰的河邊不會溼鞋,開發需求不變的項目暢通無阻(Walking on water and developing software from a specification are easy if both are frozen)
-Edward V. Berard
Farhana:你的意思是軟件開發說明書會不斷變化?
Shubho:很是正確!軟件開發惟一的真理是「軟件必定會變化」。爲何?
由於你的軟件解決的是現實生活中的業務問題,而現實生活中得業務流程老是在不停的變化。
假設你的軟件在今天工做的很好。但它能靈活的支持「變化」嗎?若是不能,那麼你就沒有一個設計敏捷的軟件。
Farhana:好,那麼請解釋一下「設計敏捷的軟件」。
Shubho:"一個設計敏捷的軟件能輕鬆應對變化,能被擴展,而且能被複用。"
而且應用好"面向對象設計"是作到敏捷設計的關鍵。那麼,你何時能說你在代碼中很好的應用了OOD?
Farhana:這正是個人問題。
Shubho:若是你代碼能作到如下幾點,那麼你就正在OOD:
面向對象
複用
能以最小的代價知足變化
不用改變現有代碼知足擴展
Farhana:還有?
Shubho:咱們並非孤立的。不少人在這個問題上思考了不少,也花費了很大努力,他們試圖作好OOD,併爲OOD指出幾條基本的原則(那些靈感你能用之於你的OOD)。他們最終也確實總結出了一些通用的設計模式(基於基本的原則)。
Farhana:你能說幾個嗎?
Shubho:固然。這裏有不少涉及原則,但最基本的是叫作SOLID的5原則(感謝Uncle Bob,偉大OOD導師)。
S = 單一職責原則 Single Responsibility Principle
O = 開放閉合原則 Opened Closed Principle
L = Liscov替換原則 Liscov Substitution Principle
I = 接口隔離原則 Interface Segregation Principle
D = 依賴倒置原則 Dependency Inversion Principle
接下去,咱們會仔細探討每個原則。
單一職責原則
Shubho:我先給你展現一張海報。咱們應當謝謝作這張海報的人,它很是有意思。
單一職責原則海報
它說:"並非由於你能,你就應該作"。爲何?由於長遠來看它會帶來不少管理問題。
從面向對象角度解釋爲:"引發類變化的因素永遠不要多於一個。"
或者說"一個類有且只有一個職責"。
Farhana:能解釋一下嗎?
Shubho:固然,這個原則是說,若是你的類有多於一個緣由會致使它變化(或者多於一個職責),你須要一句它們的職責把這個類拆分爲多個類。
Farhana:嗯...這是否是意味着在一個類裏不能有多個方法?
Shubho:不。你固然能夠在一個類中包含多個方法。問題是,他們都是爲了一個目的。現在爲何拆分是重要的?
那是由於:
每一個職責是軸向變化的;
若是類包含多個職責,代碼會變得耦合;
Farhana:能給我一個例子嗎?
Shubho:固然,看一下下面的類層次。固然這個例子是從Uncle Bob那裏得來,再謝謝他。
違反單一職責原則的類結構圖
這裏,Rectangle類作了下面兩件事:
計算矩形面積;
在界面上繪製矩形;
而且,有兩個應用使用了Rectangle類:
計算幾何應用程序用這個類計算面積;
圖形程序用這個類在界面上繪製矩形;
這違反了SRP(單一職責原則);
Farhana:如何違反的?
Shubho:你看,Rectangle類作了兩件事。在一個方法裏它計算了面積,在另一個方法了它返回一個表示矩形的GUI。這會帶來一些有趣的問題:
在計算幾何應用程序中咱們必須包含GUI。也就是在開發幾何應用時,咱們必須引用GUI庫;
圖形應用中Rectangle類的變化可能致使計算幾何應用變化,編譯和測試,反之亦然;
Farhana:有點意思。那麼我猜咱們應該依據職責拆分這個類,對嗎?
Shubho:很是對,你猜咱們應該作些什麼?
Farhana:固然,我試試。下面是咱們可能要作的:
拆分職責到兩個不一樣的類中,如:
Rectangle:這個類應該定義Area()方法;
RectangleUI:這個類應繼承Rectangle類,並定義Draw()方法。
Shubho:很是好。在這裏,Rectangle類被計算幾何應用使用,而RectangleUI被圖形應用使用。咱們甚至能夠分離這些類到兩個獨立的DLL中,那會容許咱們在變化時不須要關心另外一個就能夠實現它。
Farhana:謝謝,我想我明白SRP了。SRP看起來是把事物分離成分子部分,以便於能被複用和集中管理。咱們也不能把SRP用到方法級別嗎?個人意思是,咱們能夠寫一些方法,它們包含作不少事的代碼。這些方法可能違反SRP,對嗎?
Shubho:你理解了。你應當分解你的方法,讓每一個方法只作某一項工做。那樣容許你複用方法,而且一旦出現變化,你能購以修改最少的代碼知足變化。
開放閉合原則
Shubho:這裏是開放閉合原則的海報
開放閉合原則海報
從面向對象設計角度看,它能夠這麼說:"軟件實體(類,模塊,函數等等)應當對擴展開放,對修改閉合。"
通俗來說,它意味着你應當能在不修改類的前提下擴展一個類的行爲。就好像我不須要改變個人身體而能夠穿上衣服。
Farhana:有趣。你可以按照你意願穿上不一樣的衣服來改變面貌,而從不用改造身體。你對擴展開放了,對不?
Shubho:是的。在OOD裏,對擴展開發意味着類或模塊的行爲可以改變,在需求變化時咱們能以新的,不一樣的方式讓模塊改變,或者在新的應用中知足需求。
Farhana:而且你的身體對修改是閉合的。我喜歡這個例子。當須要變化時,核心類或模塊的源代碼不該當改動。你能用些例子解釋一下嗎?
Shubho:固然,看下面這個例子。它不支持"開放閉合"原則。
違反開發閉合原則的類結構
你看,客戶端和服務段都耦合在一塊兒。那麼,只要出現任何變化,服務端變化了,客戶端同樣須要改變。
Farhana:理解。若是一個瀏覽器以緊耦合的方式按照指定的服務器(好比IIS)實現,那麼若是服務器由於某些緣由被其餘服務器(如Apache)替換了,那麼瀏覽器也須要修改或替換。這確實很可怕!
Shubho:對的。下面是正確的設計。
遵循開放閉合原則的類結構
在這個例子中,添加了一個抽象的服務器類,客戶端包含一個抽象類的引用,具體的服務類實現了抽象服務類。那麼,因任何緣由引發服務實現發生變化時,客戶端都不須要任何改變。
這裏抽象服務類對修改是閉合的,實體類的實現對擴展是開放的。
Farhana:我明白了,抽象是關鍵,對嗎?
Shubho:是的,基本上,你抽象的東西是你係統的核心內容,若是你抽象的好,極可能在擴展功能時它不須要任 何修改(就像服務是一個抽象概念)。若是在實現裏定義了抽象的東西(好比IIS服務器實現的服務),代碼要儘量以抽象(服務)爲依據。這會容許你擴展抽 象事物,定義一個新的實現(如Apache服務器)而不須要修改任何客戶端代碼。
Liskov's 替換原則
Shubho:"Liskov's替換原則(LSP)"聽起來很難,倒是頗有用的基本概念。看下這幅有趣的海報:
Liskov替換原則海報
這個原則意思是:"子類型必須可以替換它們基類型。"
或者換個說法:"使用基類引用的函數必須能使用繼承類的對象而沒必要知道它。"
Farhana:很差意思,聽起來有點困惑。我認爲這個OOP的基本原則之一。也就是多態,對嗎?爲何一個面向對象原則須要這麼說呢?
Shubho:問的好。這就是你的答案:
在基本的面向對象原則裏,"繼承"一般是"is a"的關係。若是"Developer" 是一個"SoftwareProfessional",那麼"Developer"類應當繼承"SoftwareProfessional"類。在類設計 中"Is a"關係很是重要,但它容易衝昏頭腦,結果使用錯誤的繼承形成錯誤設計。
"Liskov替換原則"正是保證繼承可以被正確使用的方法。
Farhana:我明白了。有意思。
Shubho:是的,親愛的,確實。咱們看個例子:
Liskov替換原則類結構圖
這裏,KingFisher類擴展了Bird基類,並繼承了Fly()方法,這看起來沒問題。
如今看下面的例子:
違反Liskov替換原則類結構圖
Ostrich(鴕鳥)是一種鳥(顯然是),並從Bird類繼承。它能飛嗎?不能,這個設計就違反了LSP。
因此,即便在現實中看起來沒問題,在類設計中,Ostrich不該該從Bird類繼承,這裏應該從Bird中分離一個不會飛的類,Ostrich應該繼承與它。
Farhana:好,明白了。那麼讓我來試着指出爲何LSP這麼重要:
若是沒有LSP,類繼承就會混亂;若是子類做爲一個參數傳遞給方法,將會出現未知行爲;
若是沒有LSP,適用與基類的單元測試將不能成功用於測試子類;
對嗎?
Shubho:很是正確。你能設計對象,使用LSP作爲一個檢查工做來測試繼承是否正確。
接口分離原則
Shubho:今天咱們學習"接口分離原則",這是海報:
接口分離原則海報
Farhana:這是什麼意思?
Shubho:它的意思是:"客戶端不該該被迫依賴於它們不用的接口。"
Farhana:請解釋一下。
Shubho:固然,這是解釋:
假設你想買個電視機,你有兩個選擇。一個有不少開關和按鈕,它們看起來很混亂,且好像對你來講不必。另外一個只有幾個開關和按鈕,它們很友好,且適合你使用。假定兩個電視機提供一樣的功能,你會選哪個?
Farhana:固然是隻有幾個開關和按鈕的第二個。
Shubho:對,但爲何?
Farhana:由於我不須要那些看起來混亂又對我沒用的開關和按鈕。
Shubho:以便外部可以知道這些類有哪些可用的功能,客戶端代碼也能根據接口來設計.如今,若是接口太大,包含不少暴露的方法,在外界看來會很混亂.接口包含太多的方法也使其可用性下降,像這種包含了無用方法的"胖接口"會增長類之間的耦合.你經過接口暴露類 的功能,對.一樣地,假設你有一些類,
這也引發了其餘問題.若是一個類想實現該接口,那麼它須要實現全部的方法,儘管有些對它來講可能徹底沒用.因此說這麼作會在系統中引入沒必要要的複雜度,下降可維護性或魯棒性.
接口隔離原則確保實現的接口有他們共同的職責,它們是明確的,易理解的,可複用的.
Farhana:你的意思是接口應該僅包含必要的方法,而不應包含其它的.我明白了.
Shubho:很是正確.一塊兒看個例子.
下面是違反接口隔離原則的一個胖接口
注意到IBird接口包含不少鳥類的行爲,包括Fly()行爲.如今若是一個Bird類(如Ostrich)實現了這個接口,那麼它須要實現沒必要要的Fly()行爲(Ostrich不會飛).
Farhana:確實如此。那麼這個接口必須拆分了?
Shubho:是的。這個"胖接口"應該拆分未兩個不一樣的接口,IBird和IFlyingBird,IFlyingBird繼承自IBird.
這裏若是一種鳥不會飛(如Ostrich),那它實現IBird接口。若是一種鳥會飛(如KingFisher),那麼它實現IFlyingBird.
Farhana:因此回頭看包含了不少開關和按鈕的電視機的例子,電視機制造商應該有一個電視機的圖紙,開關和按鈕都在這個方案裏。不論任什麼時候候, 當他們向製造一種新款電視機時,若是他們想複用這個圖紙,他們將須要在這個方案裏添加更多的開關和按鈕。那麼他們將無法複用這個方案,對嗎?
Shubho:對的。
Farhana:若是他們確實須要複用方案,它們應當把電視機的圖紙份爲更小部分,以便在任何須要造新款電視機的時候複用這點小部分。
Shubho:你理解了。
依賴倒置原則
Shubho:這是SOLID原則裏最後一個原則。這是海報
它的意思是:高層模塊不該該依賴底層模塊,二者都應該依賴其抽象
Shubho:考慮一個現實中的例子。你的汽車是由不少如引擎,車輪,空調和其它等部件組成,對嗎?
Farhana:是的
Shubho:好,它們沒有一個是嚴格的構建在一個單一單元裏;換句話說,它們都是可插拔的,所以當引擎或車輪出問題時,你能夠修理它(而不須要修理其它部件),甚至能夠換一個。
在替換時,你僅須要確保引擎或車輪符合汽車的設計(如汽車能使用任何1500CC的引擎或任何18寸的車輪)。
固然,汽車也可能容許你在1500CC引擎的地方安裝一個2000CC的引擎,事實上對某些製造商(如豐田汽車)是同樣的。
如今,若是你的汽車的零部件不具有可插拔性會有什麼不一樣?
Farhana:那會很可怕!由於若是汽車的引擎出故障了,你可能修理整部車或者須要買一個新的。
Shubho:是的,那麼該如何作到"可插拔性"呢?
Farhana:這裏抽象是關鍵,對嗎?
Shubho:是的,在現實中,汽車是高級模塊或實體,它依賴於低級模塊或實體,如引擎或車輪。
相比直接依賴於引擎或車輪,汽車應依賴於某些抽象的有規格的引擎或車輪,以便於若是任何引擎或車輪符合抽象,那麼它們都能組合到汽車中,汽車也能跑動。
一塊兒看下面的類圖
Shubho:注意到上面Car類有兩個屬性,它們都是抽象類型(接口)。引擎和車輪是可插拔的,由於汽車能接受任何實現了聲明接口的對象,而且Car類不須要作任何改動。
Farhana:因此,若是代碼中不用依賴倒置,咱們將面臨以下風險:
使用低級類會破環高級代碼;
當低級類變化時須要不少時間和代價來修改高級代碼;
產生低複用的代碼;
Shubho:你徹底掌握了,親愛的!
總結
Shubho:除SOLID原則外還有不少其它的面向對象原則。如:
"組合替代繼承":這是說相對於繼承,要更傾向於使用組合;
"笛米特法則":這是說"你的類對其它類知道的越少越好";
"共同封閉原則":這是說"相關類應該打包在一塊兒";
"穩定抽象原則":這是說"類越穩定,越應該由抽象類組成";
Farhana:我應該學習那些原則嗎?
Shubho:固然能夠。你能夠從整個網上學習。僅僅須要Google一下那些原則,而後嘗試理解它。固然若是有須要,儘管問我。
Farhana:在那些設計原則之上我據說過不少設計模式。
Shubho:對的。設計模式只是對一些常常出現的場景的一些通用設計建議。這些靈感主要來自於面向對象原則。你能夠把設計模式看做"框架",把OOD原則看做"規範".
Farhana:那麼接下去我將學習設計模式嗎?
Shubho:是的,親愛的。
Farhana:那會頗有意思,對嗎?
Shubho:是,那確實使人興奮。