1. 程式人生 > >對設計模式六大原則的理解

對設計模式六大原則的理解

以下內容都是我個人的一些理解,並不代表官方或主流的說法,歡迎大家一起討論。

寫在前面:有一些最基本的理解是需要首先說明的。設計模式並不是規定,也不是唯一的準則,你可以完全不用設計模式而完成你的程式碼,保證它能實現你想要的功能。所以設計模式不是非用不可的。那我們為什麼要用它呢?因為它使我們的程式結構更清晰,更有擴充套件性,而且它可以解決某些特定的問題。就像你可以頂著一頭鳥窩快樂的生活,你也可以把它好好梳理一下,做個造型,這樣既好看,又能解決你的頭髮總往眼睛裡跑的問題:)

設計模式的六大原則也不是規定,而是類似規範的東西,你應該儘量遵守。很多時候你並不能100%依照原則做事,但它們最大的好處是為你提供了一種傾向,告訴你往這個方向靠攏是好的。

開閉原則

內容:軟體應對擴充套件開放,對修改關閉

依賴倒轉原則

內容:實現應該依賴於抽象,抽象不應該依賴於實現

理解:開閉原則和依賴倒轉原則是六大原則中最基本的兩條,他們是設計模式的基礎。設計模式的根本思想可以通俗地理解成:把底層架構搭好,然後就不要去動它,其它的一切都是建立在這個底層的基礎上的。

舉個實際的例子,你要建造一座房子,你預期它有兩層,那麼當你開始建造之前,你必須畫一張非常詳細的圖紙,它有幾面圍牆,它的地基有幾層,哪些位置應該留著接通電線、水管、天然氣,哪些位置應該做特別的設計以便與上層銜接……你必須把它們全部實現確定好,因為你明白,一旦開始建造,這些東西是無法再做修改的——任何改動都可能帶來危險的因素甚至導致返工,比如你發現你忘了留出天然氣管道的位置,於是你不得不把已經鋪好的地板挖開;比如你發現你其實更想要三層這樣你可以把父母也接來,但你當初打的地基根本就承受不了再多一層樓,於是你推到一切重新來過了。

再舉個貼近工作的例子,你要寫一個軟體(或者是跟別人合作),你確認了使用者的需求以後開始搭建軟體的框架(各種介面、抽象類等)。等你搭建完成開始編寫上層程式碼時,你就不能再修改搭建好的框架了。因為你的上層程式碼大量的繼承和呼叫了這些底層的類和方法,它們之間又發生了無數的引用,你對底層的任何修改都會帶來安全隱患,留下定時炸彈。

這兩條原則是相輔相成的,開閉原則是依賴倒轉的原則,依賴倒轉又為開閉提供了基礎。可以說為了讓程式滿足開閉原則,你必須先遵從依賴倒轉原則。抽象不依賴於實現是對修改關閉的基礎,實現依賴於抽象是對擴充套件開放的核心。

里氏代換原則

內容:在任何使用基類的地方,都可以使用其子類,而不影響程式的功能。反之不一定成立

理解:這裡說任何並不很精確,但大致意思是這樣。比如一個方法接受的引數型別是IPara這個介面,那麼ParaImp作為IPara的子類,它的例項是絕對可以被傳給這個方法的。里氏代換原則最大的好處就是,它使得一個只能接受單一類的位置可以接受無限多個不同的類(當然這些類要繼承自同一父類),大大提高了程式的擴充套件性。

舉個我自己工作的例子,我在做單元測試的時候,常常需要改變一些類的行為以實現測試目的。比如一個類中會檢查時間過了24小時才會傳送一條訊息,我要傳送這條訊息當然不能等24小時,所以我需要把這裡改成10s。但我並不能修改被測的原始碼(這樣很可能引入新的bug不是嗎?),所以我會去mock這個類。那麼我怎麼用mock的類去換掉原來的真實類呢?

很多情況下這個類就想我開始說的,是一個方法的引數。如果寫這個方法的人遵循了里氏代換原則,他就會使用父類作為這個方法的引數,這樣我的mock類就可以直接傳進去,或者不是為了測試,他發現有一種情況需要6小時傳送一條訊息,他也可以方便的把這個新類的例項傳進去,而不用改動已有的程式碼。而如果他一開始沒有這麼做,他直接把24h這個類作為引數的型別了,那我的測試是沒希望了,他也無法再擴充套件這裡的功能了。

所以里氏代換原則其實可以理解成這樣一條原則:儘量多使用基類,而不是子類。

里氏代換原則是滿足開閉原則的。

合成/聚合原則

內容:儘量使用合成/聚合而不是繼承

理解:比較簡單的理解是繼承是一種強耦合,而合成聚合是弱耦合,這條原則告訴我們要儘可能的降低類與類之間的耦合度。先解釋下什麼是合成聚合吧,合成聚合其實就是”引用“的關係,類A的一個方法接受一個類B型別的引數,那麼類A就引用了類B,這是一種“聚合”;類A中有一個域是類C型別的,那麼類A也引用了類C,這是一種“合成”。合成與聚合間的界限沒那麼清晰,可以把它們都當做引用。兩個類之間有之間的關係,那麼不是繼承就是引用。

為什麼要避免使用繼承呢?因為java是單繼承的,一個類只能有一個父類,一旦繼承就不能再更改,功能就被限定了,這顯然是不利於程式的擴充套件性的。如果想獲得某個類的功能,可以去引用它,這樣你想增加別的功能時,就不會受到限制。比如某個位置只接受類A型別的引數,而你為了獲得類B的功能而去繼承了它,顯然你的類就無法再傳入這個位置了。

這條原則也是滿足開閉原則的。

迪米特法則

內容:一個類應該與儘量少的類發生聯絡

理解:這條原則可以在上一條原則的基礎上理解為:儘可能少的使用合成聚合。雖然你可以通過引用很多類來獲取不同的功能,但如果你引用了A,而A又可以訪問B,那你就不應該再引用B,而是應該通過A來訪問B,以降低引用的類的數目。

合成/聚合原則與迪米特法則其實表達了同一個思想:要儘可能降低類與類間的耦合性。至於為什麼要降低耦合,這個不難理解,耦合性越低,程式碼結構就越清晰,修改的成本也越低(想想吧,如果你的程式碼耦合性很高,那麼你修改了一處bug,你就很難確定它的影響範圍,很可能在你修改bug的同時又埋下了新的bug)。

介面隔離原則

內容:將不同的功能分離成多個單一的介面,而不是一個總介面

理解:這條原則的本質也是降低耦合性。舉個例子,一個介面的所有方法中,一部分是為“通電”服務的,一部分是為“通網”服務的,那麼這個介面就應當被拆成兩個介面,分別服務於這兩個功能。如果不這樣的話,如果我有一個類繼承自這個介面而這個類的功能是通電,它就必須把通網的方法也實現了,這顯然是沒有必要的。

使用設計模式也許很大可能會是你的程式碼量增加很多,但這不意味著像上面說的那種包含在內。不提供任何貢獻的程式碼沒有存在的必要。

總結:按我個人理解,設計模式六大原則其實可以“濃縮”為兩條,

1、依賴於抽象,並對擴充套件開放

2、儘量降低類與類之間的耦合