在學習設計模式的時候,總是被推薦先學習一下面向對象的六大原則,學習后果然受益匪淺。以下完全是我對六大基本原則的理解,和官網解釋可能有出路,而且我更多是站在設計模式的角度,而不是面向對象的角度理解,如果有什么錯誤,敬親諒解。
1.開閉原則
很多教程都把開閉原則作為這六大原則中最基本的原則,也就是說他是各個原則的核心。開閉原則指的是,一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。
至於這個具體怎么理解,我也看了很多教程,有些教程說當我們遇到新的需求,就需要我們對我們模塊繼承的形式進行擴展,而不是修改代碼。這樣的解釋貌似有道理,但是如果真的這樣做了,程序結構只會更加復雜,業務邏輯只會更不清晰,完全是一種作死的做法。當業務發生改變的時候,肯定是要修改代碼的,不需要的東西留着只會讓程序臃腫,讓維護者搞不清什么是有用的代碼,什么是已經過時的代碼。我不太相信開閉原則的真諦是讓我們走向這樣一個死胡同。
對於開閉原則,我的理解是,我們在設計軟件的時候,首先要搞清楚程序當中什么是未來可能變化的,什么是未來不會變化的。對於可能變化的東西,我們要提前給與可以對應的擴展接口。當然實際開發中,即便是我們認為這些不會變化的地方,未來還是可能變化的,這種變化就只能改代碼了,但是這種修改僅僅只是改變個別細節,整體架構往往不會變化。而對於可能變化的地方,我們要給出可以足夠擴展的空間,讓其能夠自由擴展,基本發生了重大的需求變更,整體架構也不會受影響。
例如:工廠模式中,我們將創建對象的過程封裝了起來,這樣創建對象對的過程中,創建的代碼就和調用的代碼盡可能地解除了耦合。創建過程可能是變化的,而調用過程往往是不變的。我們創建一個對象之后,需要為其初始化,設定一些配置,這個過程需要我們給出可以擴展的余地,而且要求擴展的時候不能影響調用部分,所以需要使用工廠模式,將可變的創建過程封裝起來,供不變的調用模塊。
這樣說來,開閉原則的核心是解耦了?沒錯,我認為開閉原則講的就是解構,但是他要求我們在設計的時候,重點要預判出什么地方是會發生變化的,並要為變化的地方留出余地。他強調的是對於可變部分進行解耦,使用擴展的方式而不是修改的方式應對變化,這樣可以保證程序整體不會發生大的變化。
開閉原則對於開發框架、可以被復用的組件(如jar、dll、js插件等待)尤為重要,因為這些組件必須留出足夠的空間去讓調用者去擴展自己的業務。所以我們在開發這種組件的時候api才是最難設計的,因為我們設計的api必須能滿足調用者對他的全部擴展,這樣才能實現調用者在不修改組件代碼的情況下實現自己的需求。
2.里氏替換原則
這個原則挺簡單,講的就使用接口的時候,我們必須確保子類能夠替換父類所出現的任何地方。純粹就字面的意思來講,就是父類接口必須確保所有子類都可以實現需求,而不是某一個子類。
例如,java中HashMap和LinkedHashMap都是Map的子類。但是HashMap的順序是隨機的,而LinkedHashMap是固定的。當我們需要使用一個map,此map不需要要求key順序可控的話,我們可以聲明:
Map createMap(){
return new HashMap();
}
但我們要求順序可控是,如果是這樣:
Map createMap(){
return new LinkedHashMap();
}
上述代碼就不太好了,因為HashMap也是Map的一個子類,但是他不能滿足我們的需求,所以此處必須聲明返回值類型為LinkedHashMap。例如我們在設計接口的方法的時候,如果調用者需要的是一個LinkedHashMap,我們就不能以HashMap類型做接口的聲明。
當然里氏替換原則也可以從設計角度出發,他強調我們設計的時候需要確保父類定義了的時候,就應該覆蓋到這個接口抽象出的業務的所有方法,而不需要他的子類再添加額外的擴展,同時各個子類也都該實現父類中的所有接口,只有這樣才能保證我們設計出的東西是可擴展的。
開閉原則講究擴展,里氏替換原則可以確保通過繼承這種方式的擴展是可行的,否則就無法使用繼承去擴展程序了。
例如:我們使用模板模式,做了一個類作為父類算法模板,一個子類繼承了這個模板,並且順利地完成了運行;但是另一個子類也繼承了模板類,卻無法運行,最終發現程序無法確保所有繼承模板的子類可以替換父類,這就是個失敗的模板模式。
從面相對象的角度來說,里氏替換原則是子類可以替代父類,但是從面相組件的角度看,其實是確保組件的api是完整的、不變的,子類和外界也是完全解耦的,只有這樣我們開發出的擴展才能在不破壞原有框架的基礎上運行。
3.依賴倒置原則
這個原則也是講究解耦,他指的是讓高層模塊不要依賴低層模塊。這個是個純粹的面向接口,面向模塊開發思路了,因為面向對象而言,各個對象自己的東西和外界是解耦的,因為封裝特性把它們自己的屬性都封裝起來了,所以是不會和其他對象有耦合關系的(如c++的友元除外);但是各個對象仍然是相互耦合的,最強的耦合就屬於繼承耦合了,對象組合起碼還是輕耦合,繼承是個高耦合呀。依賴倒置就是讓各個對象耦合度降低,高層模塊不能繼承底層模塊,需要底層的東西也是外界注入而不是自己創建得到;同時調用的時候也是使用接口調用,而不是依賴具體實現,並且因為是接口調用,具體實現模塊可以被任意替換了。
這樣做就可以降低各個模塊的耦合,也可以確保里氏替換原則的實現。實現里氏替換原則有什么用?當然是可以方便擴展了。
456.職責單一原則、接口隔離原則、最小知道原則(迪米特原則)
這幾個原則很像,就放到一起說吧。這幾個類都是在強調解耦(當然開閉原則、里氏替換原則、依賴倒置原則也是)。這幾個原則基本都是強調解耦,只是站的角度不一樣。職責單一原則指的是一個模塊(接口)的功能盡量是單一的,這樣不同功能的接口就不會耦合在一起了,同時維護起來也方便。接口隔離原則強調每個類繼承的接口一定要保存最少,不能繼承無用的接口,保證接口隔離原則的前提是先要保證職責單一原則。最后一個最小知道原則指的是模塊是所有的依賴都要保存最少,這一點和接口隔離原則有點重復,或者可以說接最小知道原則包含接口隔離原則,同時最小知道原則還有對外界影響最小的意味。這幾個原則說的都是類和接口設計要盡量降低耦合的問題。
其實不止面向對象開發,其他的很多都需要實現這六大基本原則。例如寫css,這個樣式表的開發和面向對象開發本來就是風馬牛不相及,但是同樣需要能夠將需求抽象出來的思路是一致的,所以同樣需要能夠支持面向對象的6大原則。例:
我們需要實現這樣的需求,要能寫一個table的樣式,而且需要保證這個table樣式能夠復用。要保證復用,就需要保證這個table樣式職責單一,不能夠涉及到table以外的樣式,這樣維護起來簡單,同時用戶只需要table,我們就不能輸出用戶不需要的樣式,否則很容易和其他樣式沖突,這就是職責單一原則的體現。
同時在實現這個table樣式的時候,要將各個子元素的樣式依賴到自己下面,例如寫的tr樣式要確保僅對應用了這個樣式的table里的tr有效,對於沒有使用這個樣式的table的tr無影響,不能因為改變了我們的tr樣式而影響了其他table的tr樣式,這體現了最小知道原則。
同時這個table所需要的其他依賴也是要越少越好,例如如果這個table依賴於bootstarp,就沒法做了。因為如果我們依賴bootstarp,其實也僅僅是依賴很少的一部分功能,絕大部分功能是我們不依賴的,不過因為bootstarp沒有單個table樣式的css(類比面向對象的接口),我會引入很多不必要的功能,這樣會對其他部分造成污染。所以我們需要引入職責單一的類庫,不能引入不需要的功能,這也與接口隔離原則所闡述的吻合。
總結:和設計模式的關系
簡單介紹了一下這幾大基本原則,再說說我對於他們和設計模式的理解。我們在學習設計模式的時候,有沒有想過為什么要學習這個,學習了設計模式有什么好處?我在工作中經常發現很多經驗欠缺的程序員,為了學習設計模式而學習設計模式,為了使用設計模式而使用設計模式,而有經驗的程序員則不會這樣。其實程序員開發的每一個程序都是從需求出發的,只有搞清楚我們一個項目的根本需求才有使用設計模式的意義。
六大原則中,最重要的肯定就是開閉原則了,我對開閉原則的理解是,你無須在每個細節上都要做到對擴展開放,對修改關閉,而是應該為了面對擴展而擴展。當有這種需求的時候,例如一個項目業務、算法可能會有重大的變化,或者本身開發的就是一個組件,這是才需要擴展,而擴展的時候才應該使用設計模式來實現這種需求,也就是說使用設計模式和我們平時開發一樣,都是滿足需求。
使用了設計模式不一定能真正地提高代碼的結構和可維護性,所以有經驗的程序員會按需求而使用設計模式。六大基本原則可以更好地幫我們如何分析,這樣才能確定是否使用設計模式。事實上設計模式的使用上我建議無招勝有招,滿足項目的額外需求(例如利於擴展、可復用、高可維護性)都是好招,無需管他是什么設計模式。
