1. 程式人生 > >Java設計模式之初學者筆記——設計模式基礎講解

Java設計模式之初學者筆記——設計模式基礎講解

前言

        最近了解了一下設計模式,起初看的是《大話設計模式》,這本書是用C#語言寫的,覺著挺有意思,其實很多模式我們都已經在用了,卻不知道這就是設計模式。所以後來買了本GOF的《設計模式——可複用面向物件軟體的基礎》打算好好鑽研下。這本書是設計模式的鼻祖,相當權威,書名中說的是“可複用面向物件軟體的基礎”,這是基礎,我對此表示比較震撼。用了三年的面嚮物件語言居然不瞭解設計模式,不知道這是基礎,看來也是白活了。我暫時瞭解到的在此基礎上還有重構等等很多比較高階概念。但這本書顯然超出了我的理解範圍,首先他用的語言是C++,我之前沒接觸過。還有就是這本書僅有248頁,可見語言之簡練,很多地方寫的特別晦澀難懂,沒有堅實的基礎的人還是不推薦這本書了。為了繼續深入的學習,我又買了一本閻巨集的《Java與模式》,共1024頁,講述的可謂詳盡。這本書不僅對GOF的23種設計模式做了比較詳細的講解還講了幾個之後出現的設計模式。但對於一些複雜的模式我還是不能很好的理解,或是對一些比較簡單模式也只是知道結構,卻不知道如何應用。總結下來,設計模式對我暫時的工作毫無幫助。但我卻對面對物件有了更加深入的認識,比如之前我一直使用介面,卻不知道為什麼,我現在仍然在使用,但我知道我為什麼這麼做了,之前是無腦的模仿,但在我瞭解了設計模式之後,我學會了思考。這種提高我覺著是我最大的收穫。

我覺著設計模式光憑死讀書是學不會的,實踐中總結,再實踐,再總結,終有一天我相信我會掌握設計這門藝術,而現在,我需要做的是把理論基礎打好,為了將來更好的實踐它。

該筆記是編者讀過《大話設計模式》、《Java與模式》和部分《設計模式——可複用面向物件軟體的基礎》之後總結出來的初學者筆記,裡面大部分摘自書記中的關鍵性語句,也有部分自己淺顯的見解,不足之處,還請見諒。

鳴謝

特別鳴謝資深程式設計師蓋超為本筆記提出寶貴意見,完成了審校工作,確保了本筆記的正確性,併為不足和缺陷的地方添加了批註。

1、我們為什麼需要設計模式?

        每一個模式描述了一個在我們周圍不斷重複發生的問題,以及該問題的解決方案的核心,這樣,你就能一次又一次地使用該方案而不必做重複的勞動。[GOF]

        上面這句話指的是城市和建築模式,但他的思想也同樣適用於面向物件的設計模式。所以架構師的英文和建築師是一樣的——Architect。我對上面這句話的理解是這樣的:設計模式就是為了達到程式碼複用的目的。(批註:模式是程式碼複用的一種方式。具體問題具體分析,需要根據情況選擇對應的模式。比如回收站要用單例模式實現。具體規則的定義可能會用到模板方法。這些需要依照具體是情況來定。)

        軟體並不是做好了就永遠不變的,它需要隨時間而增加新的功能,或改變功能,所以需要設計模式來使改動最小卻能實現我們新的功能。所以我們所有的設計模式都是為了更好的複用之前的程式碼,在最小程度改變原有程式碼的情況下把新的功能加進去,或把某些需要的功能改變。這就是設計模式的初衷。

2、設計模式基於哪些設計原則?

       設計模式是基於一定的原則的,這些原則就是設計原則。所有設計模式都在基於這些設計原則的基礎上去實現程式碼複用。所以我認為設計原則是設計模式的基礎。(批註:同時也是設計的目的)

1)開-閉原則(Open-Closed Principle 或 OCP):一個軟體實體應當對擴充套件開放,對修改關閉。[Java與模式]

       對於OCP通俗的解釋就是:軟體實體可以被擴充套件,但不可以被修改。(批註:這個原則非常重要,是設計的核心所在。)

       每個軟體推出後都會不定時的進行更新,增加新的功能,若每次增加新的功能時都要大量的修改之前的程式碼的話很有可能講之前好用的功能改壞,所以好的設計就是遵循開閉原則,每次增加新功能時只是增加新的類,儘可能少的去修改之前的程式碼。

        實現開閉原則抽象化是其中的關鍵。[Java與模式]

        我認為,開閉原則是總的原則,之後所有的設計原則都是為了更好的實現開閉原則的。因為OCP正是設計模式所追求的達到更好的程式碼複用。在儘量少的修改原有的程式碼的基礎上,可以增加新的功能、新的模組。

2)單一職責原則(Single Responsibility Principle 或 SRP):就一個類而言,應該僅有一個引起它變化的原因。[大話設計模式](批註:我的理解是面向物件後,能高度抽象        的儘量抽象出來,這樣才符合自然。)

       我對於SRP的理解就是,一個類只負責一個功能,這樣可以使程式更利於維護。但“一個功能”我認為是一個模糊的概念,“登陸”是一個功能,“登陸”裡面的“輸入密碼”又是一個功能,輸入密碼之後的“密碼加密”又是一個功能,但怎麼樣才算單一功能呢?這大概需要程式設計師自己去把握這個度。這個度,我認為就是程式的“粒度”。這裡涉及到一個專業的術語——“粒度”。對於粒度暫且這樣理解,因為我並沒有找到相關資料。

3)對可變性的封裝原則(Principle of Encapsulation of Variation 或者 EVP):找到一個系統的可變因素,將之封裝起來。[Java與模式]

        [GOF]中對於EVP的解釋:考慮你的設計中什麼可能會發生變化。與通常將焦點放到什麼會導致設計改變的思考方式正好相反,這一思路考慮的不是什麼會導致設計改變,而是考慮你允許什麼發生變化而不讓這一變化導致重新設計。

        該原則意味著兩點[Java與模式]:

        a、一種可變性不應當散落在程式碼的很多角落裡,而應當被封裝到一個物件裡。

b、一種可變性不應當與另一種可變性混合在一起。

4)里氏代換原則(Liskov Substitution Principle 或者 LSP):任何基類可以出現的地方,子類一定可以出現。[Java與模式] 注:基類就是父類(編者注)

[大話設計模式]中的解釋:一個軟體實體如果使用的是一個父類的話,那麼一定適用於其子類,而且它察覺不出父類物件和子類物件的區別。也就是說,在軟體裡面,把父類都替換成他的子類,程式的行為沒有變化。簡單的說,就是子型別必須能夠替換掉它們的父型別。

通俗的來講就是:子類可以擴充套件父類的功能,但不能改變父類原有的功能。(批註:繼承重寫的過程是一種對父類或介面標準重新實現的過程,這個過程中必須要按照約定好的標準來做。父類的定義就是標準的定義。)

它包含以下4層含義:

子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。

子類中可以增加自己特有的方法。

當子類的方法過載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入引數更寬鬆。(編者注:這裡寬鬆指的是型別)

當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。

[以上摘自mile的部落格]

那麼LSP如何去實現儘量少的更改原有程式碼而增加新的功能的呢?

[大話設計模式]是這樣解釋的:只有當子類可以替換掉父類,軟體單元的功能不受影響時,父類才被真正的複用,而子類也能夠在父類的基礎上增加新的行為。由於這種可替換性才使得父類型別的模組在無需修改的情況下就可以擴充套件。

5)依賴倒轉原則(Dependency Inversion Principle 或者 DIP):

a、 抽象不應當依賴於細節,細節應當依賴於抽象[Java與模式]

b、 要針對介面程式設計,不要針對實現程式設計[GOF]

c、 高層模組不應該依賴底層模組。兩個都應該依賴抽象[大話設計模式]

我對DIP的理解是這樣的:比如宣告一個加密介面Encryption,但它的實現類是MD5加密的EncryptionMD5Impl,也就是Encryption eMD5 = newEncryptionMD5Impl();然後在程式中呼叫的加密方法eMD5.encrypt();這裡是MD5加密,但有一天我想用Base64加密了,這種針對於介面的程式設計就可以在改很少的原有程式碼下實現增加新的功能。也就是加一個EncryptionBase64Impl實現Encryption介面,然後其餘的地方都不用改。我認為這就是DIP最基本的運用。

注:以抽象方式耦合是依賴倒轉原則的關鍵,里氏代換原則是實現依賴倒轉原則的基礎。[Java與模式]

6)介面隔離原則(Interface Segregation Principle 或者 ISP):使用多個專門的介面比使用單一的總介面要好。[Java與模式]

我對ISP沒有什麼深刻的認識,就是覺著要把一個大的介面劃分成小的介面,把功能分離,就是ISP。

 

這個就是違反ISP的例項

 

這個是遵守ISP的例項

7)組合/聚合複用原則(Composition/Aggregation Reuse Principle 或者 CARP):在一個新的物件裡面使用一些已有的物件,使之成為新物件的一部分;新的物件通過向這些物件的委派達到複用已有功能的目的。[Java與模式]

簡言之CARP就是:要儘量使用合成/聚合,儘量不要使用繼承。[Java與模式](這個是開發的一個準則。一般情況都是這樣做的。除非面對的需求是有特殊性的,比如模板方法,這個就是把繼承用到極致的一個典範。)

那麼問題來了,為什麼要儘量使用合成/聚合,不使用繼承呢?

我認為,首先,合成/聚合是一種較弱的耦合關係,而繼承確實一種強耦合,使得兩個類之間父類改變子類也隨之變化。其次,繼承沒有合成/聚合靈活,合成/聚合關係可以將功能委派給一個介面,而功能需要改變的時候只需要改變介面的實現類就好了。但繼承的實現卻是靜態的,不能這樣改變。最後繼承破壞了封裝性(書上是這麼說的,但對於此我並沒有太好的理解)。

8)迪米特法則(Law of Demeter 或者 LoD):只與你直接的朋友們通訊。[Java與模式](這個在實際應用中需要看具體的情況來定,不可以一味的套用。在抽象的基礎上降低耦合性是常見的做法。如果每一個類只有一個朋友類,反而達不到解耦的效果。)

對迪米特法則的解釋:如果兩個類不必彼此直接通訊,那麼這兩個類就不應當發生直接的相互關係。如果其中的一個類需要呼叫另一個類的某一個方法的話,可以通過第三者轉發這個呼叫。[Java與模式]

我認為LoD就是為了降低類與類之間的耦合,把功能分開,實現模組化,讓模組之間通過另一個共通的“朋友”類進行通訊,使得將來程式改動的時候只改動某一個模組,因為模組之間是通過他們共通的朋友通訊的,所以耦合度低,不會影響其他模組的功能。[Java與模式]中說LoD的主要用意是控制資訊的過載。我認為就是模組化這個意思。

此筆記由崔淼編著