前言
此文譯自CodeProject上《How I explained OOD to my wife》一文,該文章在熱門文章(Top Articles)上排名第3,讀了之後覺得非常好,就翻譯出來,供不想讀英文的同學參考學習。
作者(Shubho)的妻子(Farhana)打算重新做一名軟體工程師(她本來是,後來因為他們孩子出生放棄了),於是作者就試圖根據自己在軟體開發設計方面的經驗幫助她學習物件導向設計(OOD)。
自作者從事軟體開發開始,作者常常注意到不管技術問題看起來多複雜,如果從現實生活的角度解釋並以對答的方式討論,那麼它將變得更簡單。現在他們把在OOD方面有些富有成效的對話分享出來,你可能會發現那是一種學習OOD很有意思的方式。
下面就是他們的對話:
OOD簡介
Shubho:親愛的,讓我們開始學習OOD吧。你瞭解物件導向原則嗎?
Farhana:你是說封裝,繼承,多型對嗎?我知道的。
Shubho:好,我希望你已瞭解如何使用類和物件。今天我們學習OOD。
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:以便外部能夠知道這些類有哪些可用的功能,客戶端程式碼也能根據介面來設計.現在,如果介面太大,包含很多暴露的方法,在外界看來會很混亂.介面包含太多的方法也使其可用性降低,像這種包含了無用方法的”胖介面”會增加類之間的耦合.你通過介面暴露類的功能,對.同樣地,假設你有一些類,
這也引起了其他問題.如果一個類想實現該介面,
介面隔離原則確保實現的介面有他們共同的職責,它們是明確的,
Shubho:非常正確.一起看個例子.
注意到IBird介面包含很多鳥類的行為,包括Fly()行為.
Farhana:確實如此。那麼這個介面必須拆分了?
Shubho:是的。這個”胖介面”應該拆分未兩個不同的介面,
這裡如果一種鳥不會飛(如Ostrich),
Farhana:
Shubho:對的。
Farhana:如果他們確實需要複用方案,
Shubho:你理解了。
依賴倒置原則
Shubho:這是SOLID原則裡最後一個原則。這是海報
它的意思是:高層模組不應該依賴底層模組,兩者都應該依賴其抽象
Shubho:考慮一個現實中的例子。你的汽車是由很多如引擎,
Farhana:是的
Shubho:好,它們沒有一個是嚴格的構建在一個單一單元裡;
在替換時,你僅需要確保引擎或車輪符合汽車的設計(
當然,
現在,如果你的汽車的零部件不具備可插拔性會有什麼不同?
Farhana:那會很可怕!因為如果汽車的引擎出故障了,
Shubho:是的,那麼該如何做到”可插拔性”呢?
Farhana:這裡抽象是關鍵,對嗎?
Shubho:是的,在現實中,汽車是高階模組或實體,
相比直接依賴於引擎或車輪,
一起看下面的類圖
Shubho:注意到上面Car類有兩個屬性,
Farhana:所以,如果程式碼中不用依賴倒置,
● 使用低階類會破環高階程式碼;
●當低階類變化時需要很多時間和代價來修改高階程式碼;
● 產生低複用的程式碼;
Shubho:你完全掌握了,親愛的!
總結
Shubho:除SOLID原則外還有很多其它的物件導向原則。
“組合替代繼承”:這是說相對於繼承,要更傾向於使用組合;
“笛米特法則”:這是說”你的類對其它類知道的越少越好”;
“共同封閉原則”:這是說”相關類應該打包在一起”;
“穩定抽象原則”:這是說”類越穩定,越應該由抽象類組成”;
Farhana:我應該學習那些原則嗎?
Shubho:當然可以。你可以從整個網上學習。
Farhana:在那些設計原則之上我聽說過很多設計模式。
Shubho:對的。
Farhana:那麼接下去我將學習設計模式嗎?
Shubho:是的,親愛的。
Farhana:那會很有意思,對嗎?
Shubho:是,那確實令人興奮。