文章目錄
-
- (1)單一職責原則(Single-Resposibility Principle)與 介面隔離原則(Interface-Segregation Principle)
- 單一職責原則
- 介面隔離原則
- (2)開放封閉原則(Open-Closed principle)與 依賴倒置原則(Dependecy-Inversion Principle)
- 開放封閉原則
- 依賴倒置
- (3)Liskov替換原則(Liskov-Substituion Principle)
我們常說啥物件導向三大特性:封裝,繼承,多態.另一種說法是:抽象,繼承,動態綁定
然後就是物件導向五大設計原則,物件導向的設計其實說到底就是類的設計嘛,沒有了類就自然不能叫物件導向了.當然了像C#中還有所謂的介面(interface),把它理解成一個特殊的類好了.
我覺得物件導向的應用中最難的就是類的設計,怎麼設計好一些類沒有固定標準,只有一些參考原則.所以設計類不只是技術活,而且是個藝術活.
類設計(或者物件導向設計)五大基本原則
(1)單一職責原則(Single-Resposibility Principle)與 介面隔離原則(Interface-Segregation Principle)
單一職責原則
一個類盡量只做一件事.不過什麼叫一件事?
就像維特根斯坦說世界可以分解為事實,而事實又分解為原子事實,原子事實(由對象組成)不可再分.那這裡很明顯的就是我們無法有一個標準來確定啥是原子事實.有些人覺得一個原子事實實際上可以再分,另一些人可能覺得不可分.
所以單一職責我們別指望去精確的確定啥是一件事,一個類的界限.反正就這樣簡單的理解.通過一個類名我們憑常規思維來想象下它可能會具有的靜態屬性(成員變數),動態屬性(成員函數).就像我們平時看到一個名詞時會聯想到跟該名詞緊密相關的性質.
要舉個體現單一職責原則的最常見的例子無疑就是STL中的迭代器的設計.
有些人覺得容器跟迭代器的分離是不好的設計,覺得添加了複雜度.還不如直接把迭代器放容器裡更簡潔.不過很多人還是不這樣認為的.首先嘛類的數量越多並不代表就越複雜.另外嘛迭代器如果放到容器裡面,就會暴露容器的一些內部結構,不太符合封裝性的思想.還有就是可擴充性的問題.因為對容器的訪問遍曆會有多種需求,如果把迭代器隔離開來你可以不修改容器類,再定義些特製的迭代器就行了.這樣不管你有啥奇怪的需求只要整個對應的迭代器出來就OK.
介面隔離原則
介面依賴使用多個小的專門的介面,而不要使用一個大的總介面
其實簡單點的講與前面說的單一職責類似嘛.在C++中一個介面就是一個類,所以更加可以直接說要體現單一職責原理.而C#中的有專門的介面interface,和類區分開來的.而且C#中不像C++支援類的多繼承,只繼承介面的多繼承.所以這裡可以把介面理解成功能更小更特殊的類,一個介面可能就只要那麼幾個很少的方法就OK了.
(2)開放封閉原則(Open-Closed principle)與 依賴倒置原則(Dependecy-Inversion Principle)
開放封閉原則
開放封閉指:對擴充開放,對修改封閉.
這樣聽著肯定覺得很迷糊.所謂修改封閉,就是你之前設計好的類,類裡面的方法等就不要去修改,比如刪除掉一個類,刪除掉一個函數或者改變函數的形參表啊.
所謂擴充開放,就是在不改變之前存在的類和函數的前提下你可以添加很多功能.一般是通過繼承和多態來實現的.這樣父類可以保持原樣.只要子類中添加些新功能.
當然有時一些應用的改變導致父類中的一些函數實現細節也難免要改(細節的實現總是要跟著需求變的).所以最好的辦法是面對抽象編程,比如定義一個抽象類別,它只涉及到函數的聲明,表明要實現哪些功能.而不涉及任何的細節.於是以後不用去修改它.而只修改或添加繼承自這個抽象類別的子類.實際上凡事都是相對而言的,不涉及到細節的抽象類別改動的可能性小點,實際項目中我們也可能必須得修改抽象類別,違反開放封閉原則.設計原則只是起指導作用,而不是起約束我們的作用.我們在大部分時候盡量遵循,但如果一些特殊情況需要特殊處理就自然不用去管啥原則了.
體現開放封閉原則的例子:
一個比較特別的例子就是C#中的擴充方法.不管是已經存在的你沒法去修改的類庫,還是你自己寫的類.假如完全不讓你去改動之前的代碼,這自然是很好的體現封閉原則.那又要給原有的類擴充一些功能可咋整? 你首先想到可能是繼承下那些類,然後在子類中添加些方法.但一來嘛如果是只是要簡單的添加一點點功能,這樣再整個類出來有點奢侈,費事了.二來嘛C#中還有些特殊的sealed類,它根本不能讓你繼承的. 於是C#中出現了個特殊的特性叫擴充方法.就是你在隨便在哪定義一個靜態函數,把你想擴充的類的類名作為參數,前面記得加個this.這樣一來你定義的函數就會"綁定"到了那個類上.就彷彿那個類多了個函數了,執行個體化一該類就能調用該函數了.比較有趣的一個功能吧.C++中是沒有這功能的.
依賴倒置
依賴倒置指依賴於抽象,高層模組不依賴於底層模組,二者都同依賴於抽象;抽象不依賴於具體,具體依賴於抽象.
是不是聽著有點雲裡霧裡的啊.先來舉個例子瞧瞧,假如你畢業要去找工作了,你可能要依賴某個技能吧,於是你就靠著會C,C++,C#,Java或者matlab之類的具體技能去找工作.假如說一些企業招人也是按這樣具體的要求,就是要你具體掌握某個具體的程式設計語言來招人.這樣你找工作的人與招聘的人兩者都要依賴於程式設計語言這個細節的底層模組.這樣一依賴就會出現找工作的人選擇範圍太小了,招聘的人可供選擇的範圍也小了.
而假如把那些具體的程式設計語言的一些基礎思想抽象出來,比如對作業系統原理,編譯原理,資料結構演算法等的掌握.假如學生依照這個抽象出來的技能去找工作,而招聘者也根據這些抽象點的要求來招人.於是皆大歡喜,都有很大的選擇範圍.
在編程中就是假如有類AA與BB,而類AA與BB互相依賴,於是比較好的方法就是把AA抽象出來一個A,BB抽象出來一個B,這樣就變成A,B互相依賴了.另外AA要依賴A,繼承自它並實現具體細節嘛.
依賴倒置設計可以這樣來理解,依賴就是剛開始是具體的細節間互相依賴,我們要變成抽象類別間的依賴,降低了耦合度.然後就是有了抽象類別,繼承自它的實作類別也要依賴它.那倒置兩字咋理解呢? 一般情況我們是先關注細節,然後根據細節抽象出來一些概括的觀念出來嘛.所以按常理一般是抽象要依賴於細節的.而現在是是倒過來了,確定一個抽象類別後,那些細節的實現得以抽象出來的規範為基準.不然你要繼承了一個抽象類別,你不完全實現它的方法的話可不讓你執行個體化對象的啊.
(3)Liskov替換原則(Liskov-Substituion Principle)
替換原則就是子類必須能夠替換其基類.
看來直貌似蠻簡單啊,你甚至可能覺得這不會廢話嘛.我們平時用的類都是子類可以替換基類的啊.要不然多態可咋實現呢.實際上之所以你沒碰到違反Liskov替換原則的類一來嘛是因為這樣的情境確實不太多,另一個就是設計好的類庫肯定不會讓違反替換原則的類出現.所以你實際應用中不太容易接觸到替換原則啊.
下面來舉個違反替換原則的特殊例子:
就是正方形與長方形的問題.我們知道正方形是一個特殊的長方形.所以可以設計兩個類,正方形類繼承自長方形類.
然後有兩變數分別表示長和寬,有個計算面積的公式.假如計算面積的方法是virtual的,這樣能實現多態嘛. 在先設定長和寬後再調用計算面積的方法.我們知道正方形是長和寬相等的.如果你設定長和寬的時候不是一樣的,然後呢又是調用了正方形的面積計算公式.這樣肯定就錯了. 你可能會問咋這麼扯蛋啊,為啥把長和寬設成不一樣啊.很多設計的思想是一來為了讓你方便,二來為了讓你少犯錯誤.就是不管你怎麼使用都不會出錯,要出錯應該是在編譯時間就錯,讓意識到哪出錯了.而如果出現上面說的情況編譯器是沒法讓你知道出錯了的.
所以你這樣一個正方形類繼承自長方形類的設計是不好的(注意的一點是你違反了Liskov可替換原則並不是說就寫的代碼就出錯了,只是說設計不太合理.實際上你這樣設計代碼沒準可以正常的跑得很好呢,如果沒有出現一些特殊情況可能是一點bug也沒有.只不過設計不合理為導致一些安全隱患而已)
概括地講,物件導向設計原則仍然是物件導向思想的體現。例如,
(1)單一職責原則與介面隔離原則體現了封裝的思想,
(2)開放封閉原則體現了對象的封裝與多態,依賴倒置原則,則是多態與抽象思想的體現而
.
(3)Liskov替換原則是對對象繼承的規範,