我是怎樣教媳婦物件導向程式設計的

oschina發表於2015-12-12

 簡介

  我老婆 Farhana 想要繼續軟體開發生涯(之前因為我們的第一個孩子出生,她不得不放棄)。我已經有了一些軟體設計和開發的經驗,所以這幾天我就在試著幫助她學習OOD。

  由於我早年在軟體開發的經驗,我總是發現無論一個技術問題看上去多麼難搞,只要從現實生活的角度去解釋或用對話的方式去討論總能讓它變得更簡單。關於OOD,我們已經有了許多成果豐碩的討論,我覺得有人可能發現這是一個學習OOD有趣的方式,所以我想我應該分享出來。

  下面是我們的談話步驟:

 話題:介紹物件導向設計

  丈夫:親愛的,讓我們開始學習物件導向設計。你瞭解物件導向規範,對嗎?

  妻子:你是指封裝,繼承和多型嗎?是的,我瞭解這些規範。

  丈夫:行,我想你已經知道怎麼用類和物件了。今天我們來學習物件導向設計。

  妻子:等等。瞭解物件導向規範對物件導向程式設計來說難道不夠嗎?我的意思是,我能夠定義類,封裝屬性和方法。我能夠根據它們的關係定義類的繼承。那還有什麼呢?

  丈夫:很好的問題。物件導向規範和麵向物件程式設計完全是兩碼事。讓我展示一個現實生活中的例子來幫助你理解它們。

  我們從牙牙學語起,都是先從字母表學起的,對吧?

  妻子: 嗯。

  丈夫: 好,然後你就能認單詞了,還能通過不同的字母拼寫出不同的單詞來。慢慢的,你能通過一些基本的語法把這些單詞串成一句話。為了使句子時態正確且沒有語病,你需要用一些介詞,連詞,等等。。看下面這句話

  "I" (代詞) "want" (動詞) "to" (介詞) "learn" (動詞) "OOD" (名詞)

  通過把幾個單詞擺放妥當一句話就好了,然後用個關鍵詞來說明一下這句話的重點。

  妻子: 親愛的,你閒扯這些到底要說明什麼呢

  丈夫: 我說的這個例子跟物件導向規範很類似,物件導向規範為物件導向程式設計定義了基本的規範,它是物件導向程式設計的主要思想。物件導向規範好比基本的英語語法,這些語法教會了你怎麼用一個個單詞拼湊出一句句話來,而物件導向規範教你怎麼用類,怎麼把一些屬性和方法封裝在一個類裡,怎麼串出類之間的繼承關係。

  妻子: 啊哈,我知道了,那麼,物件導向適用於哪裡呢。

  丈夫: 聽我慢慢道來。現在,假設你想寫點有內容有題材的文章。你當然還希望寫點你比較擅長的題材的書,就會簡單造幾個句子是遠遠不夠的,對吧。你需要筆耕不輟寫出一些長篇大論,你還需要學習怎麼可以讓讀者很容易就看懂你寫的這些長篇大論。。。

  妻子:嗯,有那麼點意思。。。繼續吧

  丈夫:現在,假如你想寫本關於物件導向設計的書,你需要把這個大的課題拆分成一些小題目。把這些小題目分幾個章節寫,還得寫前言,簡介,說明,舉例,一篇裡還有很多段落。你需要設計一整本書,還得練習一些寫作技巧,讓文章讀起來淺顯易懂。這就是綜觀全域性。

  在軟體開發中,OOD就是用來解決從全域性出發考慮問題,在設計軟體的時候,類和程式碼可以模組化,可重複使用,可靈活應用,現在已經有很多前人總結出的類和物件的設計原理了,我們直接拿來用就行了,總之,歷史的車輪已經碾壓出一條清晰的車輪印,我們只要照著走就可以了。

  妻子: 哎,懂了點皮毛,還有很多要學呢。

  丈夫:不用擔心,你很快就會上手的,讓我們接著來吧。

 話題:為什麼要進行物件導向設計?

  作者:有個很重要的問題,既然我們能夠很快的建立幾個類,編寫程式並提交,為什麼我們還要關注物件導向設計?這樣不夠麼?

  妻子:恩,以前我不知道物件導向設計,我也能開發提交專案。有什麼關係?

  丈夫:好吧,先讓我給你看一個經典的引述:

"需求不變的程式開發會同行走在冰上一樣簡單。"

- Edward V. Berard

  妻子:你是指軟體開發說明書會被不斷修改?

  丈夫:非常正確!軟體開發唯一的真理是“軟體必然修改”。為什麼?

  要知道,你的軟體解決的是現實世界中的問題,而現實生活不是一成不變的。

  可能你的軟體現在執行良好。但它能靈活的支援“變化”嗎?如果不能,那它就不是一個敏捷設計的軟體。

  妻子:好,那你就解釋一下什麼叫做“敏捷設計的軟體”!

  丈夫:“一個敏捷設計的軟體能輕鬆應對變化,能被擴充套件和複用。”

  而應用“物件導向設計”是做到敏捷設計的關鍵。那麼,什麼時候你可以說你的程式應用了物件導向設計?

  妻子:我也正想問呢。

  丈夫:如果程式碼符合以下幾點,那麼你就在“物件導向設計”:

  • 物件導向
  • 複用
  • 變化的代價極小
  • 無需改程式碼即可擴充套件

  妻子:然後呢?

  丈夫:不只我們。很多人也花了很多時間和精力思考這個問題上,他們嘗試更好的進行“物件導向設計”,併為“物件導向設計”指出幾條基本的原則(你可以用在你的“物件導向設計”中)。他們也確實總結出了一些通用的設計模式(基於基本的原則)。

  妻子:你能說出一些嗎?

  丈夫:沒問題。現在有許多設計原則,但是最基本的,就是SOLID(縮寫),這五項原則。(感謝鮑勃叔叔,偉大OOD導師)。

  S  = 單一責任原則
  O = 開閉原則
  L  = Liscov替換原則
  I  = 介面隔離原則
  D = 依賴倒置原則

  在下面的討論中,我們將詳細瞭解這些。

 話題:單一功能原則

  作者:讓我們先來看圖,我們應該感謝製作這張圖的人,因為它們真的太有趣了。

單一功能原則圖

  它的意思是:“如果你可以在一個裝置中實現所有的功能,你卻不能這樣做”。為什麼呢?因為從長遠來看它增加了很多的可管理性問題。

  從物件導向角度解釋是:

  "導致類變化的因素永遠不要多於一個。"

  或者換行個說法:"一個類有且只有一個職責"。

  妻子:可以解釋一下麼?

  丈夫:當然,這個原則是說,如果有多於一個原因會導致你的類改變(或者它的職責多餘一個),你就需要根據其職責把這個類拆分為多個類。

  妻子:嗯...這是不是意味著在一個類裡不能有多個方法?

  丈夫:當然不是。你當然可以在一個類中包含多個方法。問題是,他們都是為了一個目的。那麼,為什麼拆分很重要的?

  那是因為:

  • 每個職責都是軸向變化;
  • 如果類包含多個職責,程式碼會變得耦合;

  妻子:給個例子唄?

  丈夫:木有問題啊,瞅瞅下面類的結構。其實,這個例子是 Bob 叔叔那兒來的,得謝謝他。

我是怎樣教媳婦物件導向程式設計的

違反SRP原則的類層次結構

  這裡,Rectangle 類幹了下面兩件事:

  • 計算矩形面積;
  • 在介面上繪製矩形;

  而且,有兩個程式使用了 Rectangle 類:

  • 計算幾何應用程式用這個類計算面積;
  • 圖形程式用這個類在介面上繪製矩形;

  這違反了SRP原則(單一職責原則)!

  妻子:腫麼回事?

  丈夫:你瞅瞅,Rectangle 類幹了倆不相干的事。一個方法它計算了面積,另外一個它返回一個表示矩形的 GUI 資源。這問題就有點樂了:

  • 在計算幾何應用程式裡我們得包著 GUI。就是說,寫幾何應用程式碼,我們也得引用 GUI 庫;
  • 要是為了圖形應用所改變 Rectangle 類,計算幾何應用也可能跟著變,然後還得編譯,還得測試,另一邊也是;

  妻子:是很樂。就是說,我們得根據類的職責分開寫唄?

  丈夫:必須滴。猜猜怎麼幹?

  妻子:我想想,我尋思這得這麼辦:

  我瞅著得按職責拆成兩個類:

  • Rectangle:這個類定義 Area() 方法;
  • RectangleUI:這個把 Rectangle 類繼承過來,定義 Draw() 方法。

  丈夫:很好。這麼個,計算幾何應用使 Rectangle 類,圖形應用使 RectangleUI 類。我們還可以把這倆類分到倆單獨的 DLL 中,然後改的時候就不用管另一個了。

  妻子:謝了,我大概明白 SRP 原則了一句話:SPR 就是把東西分到不能再分了,再集中化管理和複用。囔,在方法層面上,我們不也得用 SPR 原則?我是說,我們寫的方法裡有很多幹不同事兒的程式碼,這也不符合 SPR原則吧。

  丈夫:你說地不差。方法也得分開,一個方法幹一個活。這麼著你複用方法,要是改了,也不用改太多。

 話題:開閉原則

  作者:“開閉原則“圖示如下:

圖:開閉原則圖

  讓我來解釋一下,設計規則如下:

  “軟體實體(類,模組,函式等)應該對擴充套件開放,對修改關閉。”

  這意味著在最基本的層面上,你可以擴充套件一個類的行為,而無需修改。這就像我能夠穿上衣服,而對我的身體不做任何改變,哈哈。

  妻子: 太有意思啦. 你可以通過穿不同的衣服來改變你的外貌, 但是你不必為此改變自己的身體.所以你是對擴充套件開放的, 對吧?

  丈夫: 是的. 在物件導向設計中, 對擴充套件開放意味著模組/類的行為可以被擴充套件,那麼當需求變化時我們可以用各種各樣的方法制定功能來滿足需求變更或者新需求

  妻子: 除此之外你的身體是對修改關閉的. 我喜歡這個例子. 所以, 對於核心模組或類的程式碼在需要擴充套件的時候不應該被修改. 你能結合具體例子解釋下嗎?

  丈夫: 當然了, 先看下面的例子.這個就不支援 "開放-關閉" 原則:

  類的層次結構已經表明了這是違反"開放-關閉"原則的.

  你看, 客戶端類和服務端類都是具體的實現類. 因為, 如果某些原因導致服務端實現改變了, 客戶端也需要相應變化.

  妻子: 有道理. 如果一個瀏覽器的實現和一個指定的伺服器(比如IIS)緊緊的耦合在一起, 那麼如果伺服器由於某種原因替換成了另外的(比如, Apache) 瀏覽器也需要做相應的變化或者被替換掉. 多麼恐怖的一件事啊!

  丈夫: 非常正確. 因為下面的將是一種好的設計方案:

  類的層次關係展示了"開放-關閉"原則

  在這個例子中, 新增了一個抽象的Server類, 並且客戶端保持了抽象類的引用, 具體的Server類實現了這個抽象Server類. 所以, 由於某種原因Server的實現類發生了改變, 客戶端不需要做任何改變.

  這裡的抽象的Server類對修改關閉, 具體的Server實現類對擴充套件開放.

  妻子: 我的理解是, 抽象是關鍵, 對嗎?

  丈夫: 是的, 基本上, 你要對系統的核心業務進行抽象, 如果你抽象化做的比較好, 很可能, 在擴充套件功能的時候它們不必做任何改變 (比如Server就是一個抽象的概念).  你所定義的抽象的實現 (比如, IIS伺服器 實現了 Server) 和 抽象的程式碼 (Server) 要儘可能的多. 這樣在客戶端程式碼中不需要做任何修改就會允許你定義一個新的實現(比如, ApacheServer) .

 主題: 里氏替換原則

  丈夫: "里氏替換原則"聽起來非常的複雜,但是設計思想卻是非常基礎的. 看下面這個有趣的海報

里氏替換原則海報

  原則描述了:

  "子型別必須能夠替換它們的基類."

  或者, 換句話說:

  "使用基類引用的函式必須能夠使用派生類而無須瞭解派生類."

  妻子: 對不起, 這聽起來讓我覺得有點亂. 我認為這個是物件導向程式設計的基本原則. 這個叫做多型性, 對吧? 為什麼物件導向設計原則需要考慮這個問題?

  丈夫: 非常好的問題. 這有一些答案:

  在基本的物件導向原則中, "繼承" 通常被描述成 "is a" 的關係. 如果一個 "開發者" 是"軟體專業人員", 那麼 "開發者" 類 應該 繼承 "軟體開發人員" 類. 這樣的 "Is a" 關係 在類設計階段非常重要, 但是這也很容易讓設計者得意忘形從而以一個糟糕的繼承設計告終.

  "里氏替換原則" 僅僅是一種確保繼承被正確使用的手段.

  妻子:我明白了。真有趣。

  丈夫:是的,親愛的,確實如此。讓我們來看看一個例子:

  類層次結構圖展示的是一個Liskov替換原則的例子.因為 KingFisher類擴充(繼承)了Bird類,因此繼承了Fly()這個方法,這是非常不錯的.

  我們再來看看下面的例子

修正過的Liskov替換原則的類層次結構圖

  Ostrich(鴕鳥)是一種鳥(顯然是),並繼承了 Bird 類。但它能飛嗎?不能,這個設計就違反了里氏替換原則。

  因此,即使在現實中看上去沒什麼問題,在類設計中,Ostrich 都不應該繼承 Bird 類,而應該從 Bird 中分出一個不會飛的類,由 Ostrich 繼承。

  妻子:好吧,明白了。我說說為什麼里氏替換原則如此重要:

  • 如果不遵循 LSP原則,類繼承就會混亂。如果子類例項被作為引數傳遞給方法,後果難以預測。
  • 如果不遵循 LSP原則,基於父類編寫的單元測試程式碼將無法成功執行子類。

  我說的對嗎?

  作者:完全正確,你可以設計一個物件並用LSP作為驗證工具來測試該物件是否能夠繼承。

 話題:介面隔離原則

  作者:今天我們講下“介面隔離原則”,看看下面這張海報

介面隔離原則海報

  妻子:這是什麼意思?

  作者:它的意思是這樣的:“使用者不應該被迫依賴他們不使用的介面。”

  妻子:解釋一下。

  作者:好吧,解釋如下:

  假設你想去買一臺電視機並且有兩種型別可以選擇,其中一種有很多開關和按鈕,但是多數對你來說用不到,另一種只有幾個開關和按鈕,並且看來你很熟悉怎麼用。如果這兩種電視機提供同樣的功能,你會選擇哪一種?

  妻子:當然是第二種了。

  作者:嗯,但是為什麼呢?

  妻子:因為我不需要看起來很麻煩而且對我也不必要的開關和按鈕。

  丈夫:正確。同樣的,假如你有一些類,你通過介面暴露了類的功能,這樣外部就能夠知道類中可用的功能,客戶端也可以根據介面來設計。當然那,如果介面太大,或是暴露的方法太多,從外部看也會很混亂。介面包含的方法太多也會降低可複用性, 這種包含無用方法的”胖介面“無疑會增加類的耦合。

  這還會引起其他的問題。如果一個類檢視實現介面,它需要實現介面中所有的方法,哪怕一點都用不到。所以,這樣會增加系統複雜度,降低系統可維護性和穩定性。

  介面隔離原則確保介面實現自己的職責,且清晰明確,易於理解,具有可複用性。

  妻子:我明白了,你的意思是介面只應該包括必要的方法而不是所有的。

  作者:是的,讓我們看一個例子。

  下面的介面是一個“胖介面”,這違反介面隔離原則:

違反介面隔離原則的介面示例

  注意,IBird介面定義 Fly()的行為有許多鳥類的行為。現在,如果一隻鳥類(比方說,鴕鳥)實現了這個介面,它將會實現不必要的Fly()的行為(鴕鳥不會飛)。

  妻子:是啊。因此,這個介面必須被分割?

  作者:是的,“胖介面”應該分隔成兩個不同的介面,IBird 和IFlyingBird,而IFlyingBird繼承於IBird。

介面隔離原則的例子中正確版本的介面

  如果有一隻不會飛的鳥(比如,駝鳥),只要用IBird介面即可,如果有一保會飛的鳥(比如,翠鳥),只要用IFlyingBird介面即可。

  妻子:所以,回過頭來看有很多按鈕開關的電視的例子,製造商應該有電視機的圖紙,開關和按鈕也在這個方案裡。若他們想造一臺新款電視機時想要複用這張圖紙,他們必須新增更多的按鈕和開關,否則沒法複用,對麼?

  丈夫:對。

  妻子:若是他們真的想要複用這個方案,他們應該將電視機的圖紙分為更小的部分,才能在以後製造新款電視機的時候複用這些設計方案。

  丈夫:你理解了。

 話題:依賴倒置原則

  作者:這是SOLID原則中最後的原則。圖示如下:

依賴倒置原則圖示

  它的意思是:

  “高層次的模組不應該依賴於低層次的模組,而是,都應該依賴於抽象。”

  作者:我們用一個現實的例子來理解。你的汽車是用很多部件組成,比如發動機,車輪,空調和其他的部件,是吧?

  妻子:是啊,當然是這樣。

  丈夫:你看,它們並沒有嚴格的構建在一個部件裡;就是說,它們都是“外掛”,要是引擎或著車輪出了問題,你可以單獨修理它,甚至換一個用。

  替換時,你只需要保證沉淪符合汽車的設計(汽車能使用任何1500CC的引擎或任何18寸的車輪)。

  當然,你可以在1500CC 的位置上安裝2000 CC的引擎,對某些製造商都一樣(豐田汽車)。

  可如果你的汽車部件不是“可拔插”的呢?

  妻子:那太可怕了!這樣的話,要是汽車引擎故障,你得整車修理,或者買一輛新車!

  丈夫:是的,那麼怎麼做到"可插拔"呢?

  妻子:關鍵是”抽象“,是吧?

  丈夫:對。現實世界中,汽車是高層級的模組/實體,它依賴於底層級的模組/實體,例如引擎和輪子。

  相較於直接依賴於實體的引擎或輪子,汽車應該依賴於抽象的引擎或輪子的規格,這樣只要是符合這個抽象規格的引擎或輪子,都可以裝到車裡跑。

  來看看下面的圖:

依賴倒置原則的類層次結構

  丈夫:注意上面的 Car類,它有兩個屬性,且都是抽象型別(介面)而非實體的。

  引擎和車輪是可插拔的,這樣汽車能接受任何實現了宣告介面的物件,且 Car 類無需任何改動。

  妻子:所以,如果程式碼不遵循依賴倒置,就有下面的風險:

  • 使用低層級類會破環高層級程式碼;
  • 當低層級的類變化時,需要太多時間和代價來修改高層級程式碼;
  • 程式碼可複用性不高

  丈夫:親愛的,你說到點子上了!

 總結

  丈夫:除 SOLID 原則外還有很多別的物件導向原則。比如:

  • “組合替代繼承”:是說“用組合比用繼承好”;
  • “笛米特法則”:是說“類對其它類知道的越少越好”;
  • “共同封閉原則”:是說“相關類應該一起打包”;
  • “穩定抽象原則”:這是說"類越穩定,就越應該是抽象類";

  妻子:我得學習這些原則嗎?

  丈夫:當然了。你可以在網上學習。Google 它,學習它,理解它。有問題就找我。

  妻子:我聽說還有些根據設計原則編寫的設計模式。

  丈夫:對的。設計模式不過就是針對一些經常出現的場景的一些通用的設計建議。主要的想法還是物件導向原則。你可以認為設計模式是“框架”,OOD 原則是“規範”。

  妻子:那麼之後我將學習設計模式是吧?

  丈夫:是的,親愛的。

  妻子:應該會很有意思。

  丈夫:必須地!

  原文地址:http://www.codeproject.com

相關文章