關於設計模式

weixin_33912445發表於2016-12-20

簡介

專案開發中發現問題、解決問題這個過程中會出現很多問題,比如重複出現、某個問題的遺留,這些問題的本質就是設計模式。今天記錄設計模式的知識點。

內容

Java以及其他的物件導向設計模式中,類與類之間主要有6種關係,他們分別是:依賴、關聯、聚合、組合、繼承、實現。它們的耦合度依次增強。

依賴關係:

對於兩個相對獨立的物件,當一個物件負責構造另一個物件的例項,或者依賴另一個物件的服務時,這兩個物件之間主要體現為依賴關係。

關聯關係:

分為單向關聯和雙向關聯。在java中,單向關聯表現為:類A當中使用了類B,其中類B是作為類A的成員變數。雙向關聯表現為:類A當中使用了類B作為成員變數;同時類B中也使用了類A作為成員變數。

聚合關係:

是關聯關係的一種,耦合度強於關聯,他們的程式碼表現是相同的,僅僅是在語義上有所區別:關聯關係的物件間是相互獨立的,而聚合關係的物件之間存在著包容關係,他們之間是“整體-個體”的相互關係。

組合關係:

是一種耦合度更強的關聯關係。存在組合關係的類表示“整體-部分”的關聯關係,“整體”負責“部分”的生命週期,他們之間是共生共死的;並且“部分”單獨存在時沒有任何意義。

繼承:

表示類與類(或者介面與介面)之間的父子關係。

實現:

表示一個類實現一個或多個介面的方法。

設計原則

要點定義描述

單一職責原則不要存在多於一個導致類變更的原因。通俗的說,即一個類只負責一項職責。問題由來:類T負責兩個不同的職責:職責P1,職責P2。當由於職責P1需求發生改變而需要修改類T時,有可能會導致原本執行正常的職責P2功能發生故障。

解決方案:遵循單一職責原則。分別建立兩個類T1、T2,使T1完成職責P1功能,T2完成職責P2功能。這樣,當修改類T1時,不會使職責P2發生故障風險;同理,當修改T2時,也不會使職責P1發生故障風險。

里氏替換原則定義1:如果對每一個型別為 T1的物件 o1,都有型別為 T2 的物件o2,使得以 T1定義的所有程式 P 在所有的物件 o1 都代換成 o2 時,程式 P 的行為沒有發生變化,那麼型別 T2 是型別 T1 的子型別。

定義2:所有引用基類的地方必須能透明地使用其子類的物件。問題由來:有一功能P1,由類A完成。現需要將功能P1進行擴充套件,擴充套件後的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。

解決方案:當使用繼承時,遵循里氏替換原則。類B繼承類A時,除新增新的方法完成新增功能P2外,儘量不要重寫父類A的方法,也儘量不要過載父類A的方法。

依賴倒置原則高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。問題由來:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的程式碼來達成。這種場景下,類A一般是高層模組,負責複雜的業務邏輯;類B和類C是低層模組,負責基本的原子操作;假如修改類A,會給程式帶來不必要的風險。

解決方案:將類A修改為依賴介面I,類B和類C各自實現介面I,類A通過介面I間接與類B或者類C發生聯絡,則會大大降低修改類A的機率。

介面隔離原則客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。問題由來:類A通過介面I依賴類B,類C通過介面I依賴類D,如果介面I對於類A和類B來說不是最小介面,則類B和類D必須去實現他們不需要的方法。

解決方案:將臃腫的介面I拆分為獨立的幾個介面,類A和類C分別與他們需要的介面建立依賴關係。也就是採用介面隔離原則。

迪米特法則一個物件應該對其他物件保持最少的瞭解。問題由來:類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。

解決方案:儘量降低類與類之間的耦合。

開閉原則一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉。問題由來:在軟體的生命週期內,因為變化、升級和維護等原因需要對軟體原有程式碼進行修改時,可能會給舊程式碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有程式碼經過重新測試。

解決方案:當軟體需要變化時,儘量通過擴充套件軟體實體的行為來實現變化,而不是通過修改已有的程式碼來實現變化。

設計模式

要點定義描述

單例模式確保一個類只有一個例項,而且自行例項化並向整個系統提供這個例項。單例模式注意事項:

只能使用單例類提供的方法得到單例物件,不要使用反射,否則將會例項化一個新物件。不要做斷開單例類物件與類中靜態引用的危險操作。多執行緒使用單例使用共享資源時,注意執行緒安全問題。

工廠方法模式定義一個用於建立物件的介面,讓子類決定例項化哪一個類,工廠方法使一個類的例項化延遲到其子類。在工廠方法模式中,核心的工廠類不再負責所有的物件的建立,而是將具體建立的工作交給子類去做。這個核心類則搖身一變,成為了一個抽象工廠角色,僅負責給出具體工廠子類必須實現的介面,而不接觸哪一個類應當被例項化這種細節。

抽象工廠模式為建立一組相關或相互依賴的物件提供一個介面,而且無需指定他們的具體類。在以下情況下,適用於工廠方法模式:

(1) 當一個類不知道它所必須建立的物件的類的時候。

(2) 當一個類希望由它的子類來指定它所建立的物件的時候。

(3) 當類將建立物件的職責委託給多個幫助子類中的某一個,並且你希望將哪一個幫助子類是代理者這一資訊區域性化的時候。

模版方法模式定義一個操作中演算法的框架,而將一些步驟延遲到子類中,使得子類可以不改變演算法的結構即可重定義該演算法中的某些特定步驟。子類可以置換掉父類的可變部分,但是子類卻不可以改變模板方法所代表的頂級邏輯。

每當定義一個新的子類時,不要按照控制流程的思路去想,而應當按照“責任”的思路去想。換言之,應當考慮哪些操作是必須置換掉的,哪些操作是可以置換掉的,以及哪些操作是不可以置換掉的。使用模板模式可以使這些責任變得清晰。

建造者模式將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。與抽象工廠的區別:在建造者模式裡,有個指導者,由指導者來管理建造者,使用者是與指導者聯絡的,指導者聯絡建造者最後得到產品。即建造模式可以強制實行一種分步驟進行的建造過程。

建造模式是將複雜的內部建立封裝在內部,對於外部呼叫的人來說,只需要傳入建造者和建造工具,對於內部是如何建造成成品的,呼叫者無需關心。

在Java的應用中JavaMail使用到了該模式。

代理模式為其他物件提供一種代理以控制對這個物件的訪問。所謂代理,就是一個人或者機構代表另一個人或者機構採取行動。在一些情況下,一個客戶不想或者不能夠直接引用一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用。

原型模式用原型例項指定建立物件的種類,並通過拷貝這些原型建立新的物件。

原型模式要求物件實現一個可以“克隆”自身的介面,這樣就可以通過複製一個例項物件本身來建立一個新的例項。這樣一來,通過原型例項建立新的物件,就不再需要關心這個例項本身的型別,只要實現了克隆自身的方法,就可以通過這個方法來獲取新的物件,而無須再去通過new來建立。在Java語言裡深度克隆一個物件,常常可以先使物件實現Serializable介面,然後把物件(實際上只是物件的拷貝)寫到一個流裡(序列化),再從流裡讀回來(反序列化),便可以重建物件。

原型模式的優點

原型模式允許在執行時動態改變具體的實現型別。原型模式可以在執行期間,由客戶來註冊符合原型介面的實現型別,也可以動態地改變具體的實現型別,看起來介面沒有任何變化,但其實執行的已經是另外一個類例項了。因為克隆一個原型就類似於例項化一個類。

原型模式的缺點

原型模式最主要的缺點是每一個類都必須配備一個克隆方法。配備克隆方法需要對類的功能進行通盤考慮,這對於全新的類來說不是很難,而對於已經有的類不一定很容易,特別是當一個類引用不支援序列化的間接物件,或者引用含有迴圈結構的時候。

中介者模式用一箇中介者物件封裝一系列的物件互動,中介者使各物件不需要顯示地相互作用,從而使耦合鬆散,而且可以獨立地改變它們之間的互動。

中介者模式的優點

適當地使用中介者模式可以避免同事類之間的過度耦合,使得各同事類之間可以相對獨立地使用。

使用中介者模式可以將物件間一對多的關聯轉變為一對一的關聯,使物件間的關係易於理解和維護。

使用中介者模式可以將物件的行為和協作進行抽象,能夠比較靈活的處理物件間的相互作用。適用場景

在物件導向程式設計中,一個類必然會與其他的類發生依賴關係,完全獨立的類是沒有意義的。一個類同時依賴多個類的情況也相當普遍,既然存在這樣的情況,說明,一對多的依賴關係有它的合理性,適當的使用中介者模式可以使原本凌亂的物件關係清晰,但是如果濫用,則可能會帶來反的效果。一般來說,只有對於那種同事類之間是網狀結構的關係,才會考慮使用中介者模式。可以將網狀結構變為星狀結構,使同事類之間的關係變的清晰一些。

中介者模式是一種比較常用的模式,也是一種比較容易被濫用的模式。對於大多數的情況,同事類之間的關係不會複雜到混亂不堪的網狀結構,因此,大多數情況下,將物件間的依賴關係封裝的同事類內部就可以的,沒有必要非引入中介者模式。濫用中介者模式,只會讓事情變的更復雜。

命令模式意圖:將一個請求封裝為一個物件,從而可用不同的請求對客戶進行引數化;對請求排隊或記錄日誌,以及支援可撤銷的操作

動機:將”發出請求的物件”和”接收與執行這些請求的物件”分隔開來。常見應用:

1、工作佇列,執行緒池,日程安排

2、日誌請求(系統恢復)

要點:

1、命令模式將發出請求的物件和執行請求的物件解耦

2、在被解耦的兩者之間是通過命令物件進行溝通的。命令物件封裝了接收者和一個或一組動作

3、呼叫者通過呼叫命令物件的execute()發出請求,這會使得接收者的動作被呼叫

4、呼叫者可以接受命令當作引數,甚至在執行時動態的進行

5、命令可以支援撤銷,做法是實現一個undo()方法來回到execute()被執行前的狀態

6、巨集命令是命令的一種簡單的延伸,允許呼叫多個命令。巨集方法也可以支援撤銷

7、實際操作時,很常見使用"聰明"命令物件,也就是直接實現了請求,而不是將工作委託給接受者(弊端?)

8、命令也可以用來實現日誌和事物系統

責任鏈模式使多個物件都有機會處理請求,從而避免了請求的傳送者和接收者之間的耦合關係。將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有物件處理它為止。一個純的責任鏈模式要求一個具體的處理者物件只能在兩個行為中選擇一個:一是承擔責任,而是把責任推給下家。不允許出現某一個具體處理者物件在承擔了一部分責任後又 把責任向下傳的情況。

在一個純的責任鏈模式裡面,一個請求必須被某一個處理者物件所接收;在一個不純的責任鏈模式裡面,一個請求可以最終不被任何接收端物件所接收。

純的責任鏈模式的實際例子很難找到,一般看到的例子均是不純的責任鏈模式的實現。有些人認為不純的責任鏈根本不是責任鏈模式,這也許是有道理的。但是在實際的系統裡,純的責任鏈很難找到。如果堅持責任鏈不純便不是責任鏈模式,那麼責任鏈模式便不會有太大意義了。

裝飾模式又名包裝(Wrapper)模式,裝飾模式以對客戶端透明的方式擴充套件物件的功能,是繼承關係的一個替代方案。裝飾模式與類繼承的區別:

1)    裝飾模式是一種動態行為,對已經存在類進行隨意組合,而類的繼承是一種靜態的行為,一個類定義成什麼樣的,該類的物件便具有什麼樣的功能,無法動態的改變。

2)    裝飾模式擴充套件的是物件的功能,不需要增加類的數量,而類繼承擴充套件是類的功能,在繼承的關係中,如果我們想增加一個物件的功能,我們只能通過繼承關係,在子類中增加兩個方法。

3)    裝飾與繼承比較圖:

4)    裝飾模式是在不改變原類檔案和使用繼承的情況下,動態的擴充套件一個物件的功能,它是通過建立一個包裝物件,也就是裝飾來包裹真是的物件。

5.    裝飾模式把對客戶端的呼叫委派給被裝飾的類,裝飾模式的關鍵在於這種擴充套件完全透明的。

策略模式定義一組演算法,將每個演算法都封裝起來,並且使他們之間可以互換。

策略模式的好處在於你可以動態的改變物件的行為。策略模式屬於物件行為型模式,主要針對一組演算法,將每一個演算法封裝到具有共同介面的獨立的類中,從而使得它們可以相互替換。策略模式使得演算法可以在不影響 到客戶端的情況下發生變化。通常,策略模式適用於當一個應用程式需要實現一種特定的服務或者功能,而且該程式有多種實現方式時使用。

介面卡模式基於現有類所提供的服務,向客戶提供介面,以滿足客戶的期望。

介面卡模式的用意是要改變源的介面,以便於目標介面相容。預設適配的用意稍有不同,它是為了方便建立一個不平庸的介面卡類而提供的一種平庸實現。介面卡模式的優點

更好的複用性

系統需要使用現有的類,而此類的介面不符合系統的需要。那麼通過介面卡模式就可以讓這些功能得到更好的複用。

更好的擴充套件性

在實現介面卡功能的時候,可以呼叫自己開發的功能,從而自然地擴充套件系統的功能。

介面卡模式的缺點

過多的使用介面卡,會讓系統非常零亂,不易整體進行把握。比如,明明看到呼叫的是A介面,其實內部被適配成了B介面的實現,一個系統如果太多出現這種情況,無異於一場災難。因此如果不是很有必要,可以不使用介面卡,而是直接對系統進行重構。

迭代器模式提供一種方法訪問一個容器物件中各個元素,而又不暴露該物件的內部細節。在jdk中,與迭代器相關的介面有兩個:Iterator 與 Iterable

Iterator:迭代器,Iterator及其子類通常是迭代器本身的結構與方法;

Iterable:可迭代的,那些想用到迭代器功能的其它類,如AbstractList HashMap等,需要實現該介面。

組合模式將物件組合成樹形結構以表示‘部分-整體’的層次結構。組合模式使得使用者對單個物件和組合物件的使用具有一致性。

物件通過實現(繼承)統一的介面(抽象類),呼叫者對單一物件和組合物件的操作具有一致性。通過實現組合模式,呼叫者對組合物件的操作與對單一物件的操作具有一致性。呼叫者不用關心這是組合物件還是檔案,也不用關心組合物件內部的具體結構,就可以呼叫相關方法,實現功能。

觀察者模式定義物件間一種一對多的依賴關係,使得當每一個物件改變狀態,則所有依賴於它的物件都會得到通知並自動更新。

觀察者模式定義了一種一對多的依賴關係,讓多個觀察者物件同時監聽某一個主題物件。這個主題物件在狀態上發生變化時,會通知所有觀察者物件,使它們能夠自動更新自己。在JAVA語言的java.util庫裡面,提供了一個Observable類以及一個Observer介面,構成JAVA語言對觀察者模式的支援。

門面模式外部與一個子系統的通訊必須通過一個統一的門面物件進行。門面模式的優點:

●  鬆散耦合

門面模式鬆散了客戶端與子系統的耦合關係,讓子系統內部的模組能更容易擴充套件和維護。

●  簡單易用

門面模式讓子系統更加易用,客戶端不再需要了解子系統內部的實現,也不需要跟眾多子系統內部的模組進行互動,只需要跟門面類互動就可以了。

●  更好的劃分訪問層次

通過合理使用Facade,可以幫助我們更好地劃分訪問的層次。有些方法是對系統外的,有些方法是系統內部使用的。把需要暴露給外部的功能集中到門面中,這樣既方便客戶端使用,也很好地隱藏了內部的細節。

備忘錄模式在不破壞封裝性的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態。這樣就可以將該物件恢復到原先儲存的狀態。備忘錄物件是一個用來儲存另外一個物件內部狀態的快照的物件。備忘錄模式的用意是在不破壞封裝的條件下,將一個物件的狀態捕捉(Capture)住,並外部化,儲存起來,從而可以在將來合適的時候把這個物件還原到儲存起來的狀態。備忘錄模式常常與命令模式和迭代子模式一同使用。

訪問者模式封裝某些作用於某種資料結構中各元素的操作,它可以在不改變資料結構的前提下定義作用於這些元素的新的操作。

訪問者模式是物件的行為模式。訪問者模式的目的是封裝一些施加於某種資料結構元素之上的操作。一旦這些操作需要修改的話,接受這個操作的資料結構則可以保持不變。訪問者模式的優點

好的擴充套件性

能夠在不修改物件結構中的元素的情況下,為物件結構中的元素新增新的功能。

好的複用性

可以通過訪問者來定義整個物件結構通用的功能,從而提高複用程度。

分離無關行為

可以通過訪問者來分離無關的行為,把相關的行為封裝在一起,構成一個訪問者,這樣每一個訪問者的功能都比較單一。

訪問者模式的缺點

物件結構變化很困難

不適用於物件結構中的類經常變化的情況,因為物件結構發生了改變,訪問者的介面和訪問者的實現都要發生相應的改變,代價太高。

破壞封裝

訪問者模式通常需要物件結構開放內部資料給訪問者和ObjectStructrue,這破壞了物件的封裝性。

狀態模式當一個物件的內在狀態改變時允許改變其行為,這個物件看起來像是改變了其類。

狀態模式允許一個物件在其內部狀態改變的時候改變其行為。這個物件看上去就像是改變了它的類一樣。

直譯器模式給定一種語言,定義他的文法的一種表示,並定義一個直譯器,該直譯器使用該表示來解釋語言中句子。

享元模式複用我們記憶體中已存在的物件,降低系統建立物件例項的效能消耗。

Flyweight在拳擊比賽中指最輕量級,即“蠅量級”或“雨量級”,這裡選擇使用“享元模式”的意譯,是因為這樣更能反映模式的用意。享元模式是物件的結構模式。享元模式以共享的方式高效地支援大量的細粒度物件。享元模式採用一個共享來避免大量擁有相同內容物件的開銷。這種開銷最常見、最直觀的就是記憶體的損耗。享元物件能做到共享的關鍵是區分內蘊狀態(Internal State)和外蘊狀態(External State)。

橋樑模式將抽象和實現解耦,使得兩者可以獨立地變化。

橋樑模式的用意是“將抽象化(Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化”。橋樑模式在Java應用中的一個非常典型的例子就是JDBC驅動器。JDBC為所有的關係型資料庫提供一個通用的介面。一個應用系統動態地選擇一個合適的驅動器,然後通過驅動器向資料庫引擎發出指令。這個過程就是將抽象角色的行為委派給實現角色的過程。

專案

寫了一個Android的專案體現23中設計模式,專案如圖:

2382616-38c656c8d0575657
2382616-8b484566d0dd3619

測試程式碼:

[java]view plaincopy

publicvoidonClickSingleMode(View view) {// 單例

SingleMode.getInstance();

}

publicvoidonClickFactoryMethodModel(View view) {// 工廠方法

IProduct iProduct =newFactoryMethodModel();

iProduct.productMethod();

iProduct =newTree();

iProduct.productMethod();

}

publicvoidonClickAbstractFactoryModel(View view) {// 抽象工廠

AbstractFactoryModel.test();

}

publicvoidonClickTemplateMethodModel(View view) {// 模版方法模式

TemplateMethodModel.test();

}

publicvoidonClickBuilderMode(View view) {// 建造者模式

BuilderMode.test();

}

publicvoidonClickProxyMode(View view) {// 代理模式

ProxyMode.test();

}

publicvoidonClickCloneMode(View view) {// 原型模式

CloneMode.test();

}

publicvoidonClickIntermediaryModel(View view) {// 中介者模式

IntermediaryModel.test1();

IntermediaryModel.test2();

}

publicvoidonClickCommandMode(View view) {// 命令模式

CommandMode.test();

}

publicvoidonChainOfResponsibilityModel(View view) {// 責任鏈模式

ChainOfResponsibilityModel.test();

}

publicvoidonClickDecorativeMode(View view) {// 裝飾模式

DecorativeMode.test();

}

publicvoidonClickStrategyMode(View view) {// 策略模式

StrategyMode.test();

}

publicvoidonClickIteratorModel(View view) {// 模式

IteratorModel.test();

}

publicvoidonClickCombinationMode(View view) {// 組合模式

CombinationMode.test();

}

publicvoidonClickObserverMode(View view) {// 觀察者模式

ObserverMode.test();

}

publicvoidonClickWindowMode(View view) {// 門面模式

WindowMode.test();

}

publicvoidonClickMemoMode(View view) {// 備忘錄模式

MemoMode.test();

}

publicvoidonClickVisitorMode(View view) {// 訪問者模式

VisitorMode.test();

}

publicvoidonClickStateModel(View view) {// 狀態模式

StateModel.test();

}

publicvoidonClickParserMode(View view) {// 直譯器模式

ParserMode.test();

}

publicvoidonClickFlyweightMode(View view) {// 享元模式

FlyweightMode.test();

}

publicvoidonClickBridgeMode(View view) {// 橋樑模式

BridgeMode.test();

}

總結

如果設計模式在編碼設計生涯中用得極少,主要原因是對設計模式的理解還不夠,認識不到問題的存在。

因為不能正確的分析問題、認識問題,當然也不可能很好的解決問題了。

專案下載

相關文章