【摘 要】 設計模式的原則就是說模塊應對擴展開放,而對修改關閉。模塊應盡量在不修改原(是"原",指原來的代碼)代碼的情況下進行擴展。那么怎么擴展呢?我們看工廠模式"factory pattern":假設中關村有一個賣盜版盤和毛片的小子,我們給他設計一"光盤銷售管理軟件"。
到底我們為什么要用設計模式呢?這么多設計模式為什么要這么設計呢?為什么要提倡"Design Pattern"呢?根本原因是為了代碼復用,增加可維護性。那么怎么才能實現代碼復用呢?OO界有前輩的七大原則:"開-閉"原則(Open Closed Principal)、里氏代換原則、合成復用原則,依賴倒轉原則,接口隔離原則,抽象類,迪米特法則。設計模式就是實現了這些原則,從而達到了代碼復用、增加可維護性的目的。
1、"開-閉"原則
這些OOD原則的一個基石就是"開-閉原則"(Open-Closed Principle OCP).這個原則最早是由Bertrand Meyer提出,英文的原文是:Software entities should be open for extension,but closed for modification.意思是說,一個軟件實體應當對擴展開放,對修改關閉.也就是說,我們在設計一個模塊的時候,應當使這個模塊可以在不被修改的前提下被擴展,換句話說就是,應當可以在不必修改源代碼的情況下改變這個模塊的行為.
滿足OCP的設計給系統帶來兩個無可比擬的優越性.
1.通過擴展已有的軟件系統,可以提供新的行為,以滿足對軟件的新需求,使變化中的軟件系統有一定的適應性和靈活性.
2.已有的軟件模塊,特別是最重要的抽象層模塊不能再修改,這就使變化中的軟件系統有一定的穩定性和延續性.
具有這兩個優點的軟件系統是一個高層次上實現了復用的系統,也是一個易于維護的系統.那么,我們如何才能做到這個原則呢?不能修改而可以擴展,這個看起來是自相矛盾的.其實這個是可以做到的,按面向對象的說法,這個就是不允許更改系統的抽象層,而允許擴展的是系統的實現層.
解決問題的關鍵在:抽象化.我們讓模塊依賴于一個固定的抽象體,這樣它就是不可以修改的;同時,通過這個抽象體派生,我們就可以擴展此模塊的行為功能.如此,這樣設計的程序只通過增加代碼來變化而不是通過更改現有代碼來變化,前面提到的修改的副作用就沒有了.
"開-閉"原則如果從另外一個角度講述,就是所謂的"對可變性封裝原則"(Principle of Encapsulation of Variation, EVP).講的是找到一個系統的可變因素,將之封裝起來.在我們考慮一個系統的時候,我們不要把關注的焦點放在什么會導致設計發生變化上,而是考慮允許什么發生變化而不讓這一變化導致重新設計.也就是說,我們要積極的面對變化,積極的包容變化,而不是逃避.
2、里氏代換原則
指導我們如何去構建一個extends(繼承、派生)結構。
子類與父類的關系必須是is-A,即,子類必須在任何場合都敢于大聲宣稱自己起碼(至少)是一個父類。比如,假設某類結構,“男人”、“女人”從“人”派生出來,看起來就是滿足里氏代換原則的,因為無論“男人”還是“女人”,在任何場合都是“人”。這個原則大多數情況下,可以用現實世界中的概念來思考,但軟件世界與顯示世界畢竟有區別,比如,書中的例子,“正方形是不是矩形”問題。
此外,很多問題需要利用OO核心思想來靈活考慮,還是書中的例子(我只說大概意思,可能與書中描述存在差異),一個類結構,“職工、普通職員、項目經理、科長、部長……”,從“員工”類派生出來,從一般概念來看還不錯,職工和部長都是員工嘛,但這里隱含問題,現實世界中,普通職員可能變成項目經理,但軟件世界中,普通職員和項目經理被規劃成兩個類了,于是一個普通職員instance是很難變成項目經理instance的,這說明我們把顯示世界映射為軟件世界時,出現了問題。更加合理的做法是,把這些職務(角色)抽象出來,比如叫做“職務”類(interface/abstract class),所有職務從其派生,員工類、職務類是關聯關系,任何一個員工實例都一個職務實例作為它的屬性,這樣就對了,員工的職務是可以變化的。實現這個重構的依據,是OO中的封裝變化思想,以及從中演化出來的依賴倒換原則、合成/聚合復用原則。
但凡涉及到extends結構的設計模式都符合著里氏代換原則:
策略模式:一組算法,把他們封裝成對象,使之可以互換(滿足同一接口,即都is-A這個接口)。
合成模式:Leaf和Composite都is-A Component,于是才可能方便地層層嵌套。
代理模式:Proxy與RealSubject都is-A父類Subject,于是可以插入代理,完成附加功能。
3、合成復用原則
合成和聚合都是關聯的特殊種類。聚合表示“擁有”關系或整體與部分的關系;而合成則是用來表示一種強的多的 “擁有”。在一個合成的關系里,部分和整體的生命周期是一樣的。一個合成的新的對象完全用又對其他組成部分的支配權。包括他們的創建和湮滅。組合而成的對象對組成部分的內存分配、內存釋放有絕對的責任。 進一步講。一個合成的多重性不能超過1,換言之,一個合成的關系中,部分對象不能于另一個合成關系對象共享。
合成通常理解為:值的聚合。而聚合則是:引用的聚合。
合成和聚合是將已有的對象納入到新的對象中,使之成為新對象的一部分,因此新的對象可以調用已由對象的功能。達到復用的目的。
優點:
l 新對象存取成分對象的唯一方法是通過成分對象的接口。
這種復用是黑箱復用,隱藏了成分對象的內部細節。
這種復用支持包裝。
這種復用依賴性較小。(耦合度低)
每一個新類可以將焦點集中在一個任務上。
這種復用可以在運行時動態進行。新對象可以動態引用于成分對象類型的子對象。
缺點就是:
通過這種復用建造的系統通常會有較多的對象需要管理。
“合成聚合復用可以替代繼承復用完成任一項功能。”
4 依賴倒轉原則
很具體地指導我們對抽象類(接口)、實現類的使用。
依賴于抽象的實體(interface/abstract class),才能夠更具有可插入性(但凡實現既有接口的實現類實例都可以在依賴此接口的地方以此接口實例的角色插入進來),更容易滿足Open- Close原則(抽象的層次不變化、實現的層次由于使用不同的類來封裝不同的變化,于是可以在增加新類作為擴展的同時不需要修改已有實現類)。
5 接口隔離原則
含 義:使用多個專門的接口要比使用單一的總接口要好!
從客戶類角度上看:一個類對另外一個類的依賴性應當是建立在最小的接口之上。
接口的劃分直接帶來類型的劃分。
目標就是不向客戶類提供不必要的行為。
6 抽象類
抽象類不會有實例,一般作為父類為子類繼承,一般包含這個系的共同屬性和方法。
注意:好的繼承關系中,只有葉節點是具體類,其他節點應該都是抽象類,也就是說具體類
是不被繼承的。將盡可能多的共同代碼放到抽象類中。
7 迪米特法則
用于解開類之間的不必要的耦合。“不要與陌生人說話”。說起來容易,實際操作的時候很可能出現些無所適從的問題,而解開類之間的耦合是比較重要的,目前我們設計類的時候,增加屬性、增加對其他類的調用都比較隨意,不太好。
迪米特法則要求:一個對象應當對其他對象有盡可能少的了解。幾種其他的表述:只與你直接的朋友通信、不要跟“陌生人”說話(馮遠征?)
每個軟件單位對其他單位都具有最少的知識,而且局限于那些與本單位密切相關的軟件單位。
狹義迪米特法則-規定了誰是Friends、誰是Stranger。Friends圈子包括:
當前對象自身(this)、Instance field(如果field是Map、List等容器類型,則容器內的對象也是朋友)、當前對象創建的對象(調用其構造方法)、當前對象方法的參數對象。
只跟Friends說話,跟Stranger說的話由Friends轉述。
狹義迪米特法則的弊端:傳遞間接調用的小方法太多!解決辦法:遵循依賴倒換原則做些折衷處理,讓對象依賴于Stranger的抽象層。雖然沒有完全斷開耦合,至少降低了耦合。
總結: 軟件系統中,一個模塊設計的好不好最主要、最重要的標志,就是該模塊在多大程度上將自己的內部數據和其他實現細節隱藏起來。一個設計好的模塊可以將它所有的實現細節隱藏起來,很徹底的將提供給外界的API和自己的實現分離開來。這樣一來模塊于模塊之間的通信僅僅通過彼此的API,而不理會模塊內部的工作細節。這便是:“信息的隱藏”。