設計模式之零:開篇
設計模式介紹
模式:每一個模式描述了一個在我們周圍不斷重複發生的問題,以及該問題的解決方案的核心。
“模式”這個詞是不局限於軟件開發行業的,它幾乎無處不在,它其實就是一種經驗的積累,就象大多數人的教育經曆都是從小學到初中再到高中再到大學,這也是一種模式,是中國的教育模式;現在越來越火的出國熱,也是另一種模式,海外留學模式。因為GOF的《設計模式:可複用麵向對象軟件的基礎》一書描述的23種經典設計模式,奠定了模式在軟件行業的地位,從此人們提到“設計模式”就是默指“麵向對象設計模式”,但是如前文所述,模式絕對不局限於軟件行業,即使在軟件行業,也不局限於GOF描述的23種設計模式,例如最著名的MartinFlower的《企業架構模式》,還有我們常用的MVC、IOC等。
因為模式是一種經驗的積累和總結,所以通過模式,我們可以站在巨人的肩膀上去思考問題、解決問題,熟練使用設計模式可以提高我們的工作效率,改善產品質量,最終帶來經濟效益。因此對於任何想開發出靈活高效、健壯的軟件產品的個人或團體,熟練掌握並正確使用設計模式都是必須掌握的基本技能。
所以,讓我們開始吧……
GRASP (職責分配原則)
要學習設計模式,有些基礎知識是我們必須要先知道的,設計模式是關於類和對象的一種高效、靈活的使用方式,也就是說,必須先有類和對象,才能有設計模式的用武之地,否則一切都是空談,那麼類和對象是從那冒出來的呢?這時就需要比23種設計模式更重要更經典的GRASP模式登場了,嘿嘿,原來這才是老大!
GRASP(General Responsibility Assignment Software Patterns),中文名稱為“通用職責分配軟件模式”,GRASP一共包括9種模式,它們描述了對象設計和職責分配的基本原則。也就是說,如何把現實世界的業務功能抽象成對象,如何決定一個係統有多少對象,每個對象都包括什麼職責,GRASP模式給出了最基本的指導原則。初學者應該盡快掌握、理解這些原則,因為這是如何設計一個麵向對象係統的基礎。可以說,GRASP是學習使用設計模式的基礎。
1. Information Expert (信息專家)
信息專家模式是麵向對象設計的最基本原則,是我們平時使用最多,應該跟我們的思想融為一體的原則。也就是說,我們設計對象(類)的時候,如果某個類擁有完成某個職責所需要的所有信息,那麼這個職責就應該分配給這個類來實現。這時,這個類就是相對於這個職責的信息專家。
例如:常見的網上商店裏的購物車(ShopCar),需要讓每種商品(SKU)隻在購物車內出現一次,購買相同商品,隻需要更新商品的數量即可。如下圖:

2. Creator (創造者)
實際應用中,符合下列任一條件的時候,都應該由類A來創建類B,這時A是B的創建者:
a. A是B的聚合
b. A是B的容器
c. A持有初始化B的信息(數據)
d. A記錄B的實例
e. A頻繁使用B
如 如果一個類創建了另一個類,那麼這兩個類之間就有了耦合,也可以說產生了依賴關係。依賴或耦合本身是沒有錯誤的,但是它們帶來的問題就是在以後的維護中會產生連鎖反應,而必要的耦合是逃不掉的,我們能做的就是正確地創建耦合關係,不要隨便建立類之間的依賴關係,那麼該如何去做呢?就是要遵守創建者模式規定的基本原則,凡是不符合以上條件的情況,都不能隨便用A創建B。

這裏因為訂單是商品的容器,也隻有訂單持有初始化商品的信息,所以這個耦合關係是正確的且沒辦法避免的,所以由訂單來創建商品。
3. Low coupling (低耦合)
耦合模式的意思就是要我們盡可能地減少類之間的連接。
低耦合作用非常重要:
a. 低耦合降低了因一個類的變化而影響其他類的範圍。
b. 低耦合使類更容易理解,因為類會變得簡單,更內聚。 下麵這些情況會造成類A、B之間的耦合:
a. A是B的屬性
b. A調用B的實例的方法
c. A的方法中引用了B,例如B是A方法的返回值或參數。
d. A是B的子類,或者A實現了B
關於低耦合,還有下麵一些基本原則:
a. Don’t Talk to Strangers原則:
意思就是說,不需要通信的兩個對象之間,不要進行無謂的連接,連接了就有可能產生問題,不連接就一了百了啦!
b. 如果A已經和B有連接,如果分配A的職責給B不合適的話(違反信息專家模式),那麼就把B的職責分配給A。
c. 兩個不同模塊的內部類之間不能直接連接,否則必招報應!嘿!
例如:Creator模式的例子裏,實際業務中需要另一個出貨人來清點訂單(Order)上的商品(SKU),並計算出商品的總價,但是由於訂單和商品之間的耦合已經存在了,那麼把這個職責分配給訂單更合適,這樣可以降低耦合,以便降低係統的複雜性。如下圖:
這裏我們在訂單類裏增加了一個TotalPrice()方法來執行計算總價的職責,沒有增加不必要的耦合。
4. High
cohesion (高內聚)
高內聚的意思是給類盡量分配內聚的職責,也可以說成是功能性內聚的職責。即功能性緊密相關的職責應該放在一個類裏,並共同完成有限的功能,那麼就是高內聚合。這樣更有利於類的理解和重用,也便於類的維護。 例如:一個訂單數據存取類(OrderDAO),訂單即可以保存為Excel模式,也可以保存到數據庫中;那麼,不同的職責最好由不同的類來實現,這樣才是高內聚的設計,如下圖:

這裏我們把兩種不同的數據存儲功能分別放在了兩個類裏來實現,這樣如果未來保存到Excel的功能發生錯誤,那麼就去檢查OrderDAOExcel類就可以了,這樣也使係統更模塊化,方便劃分任務,比如這兩個類就可以分配個不同的人同時進行開發,這樣也提高了團隊協作和開發進度。
5. Controller
(控製器)
用來接收和處理係統事件的職責,一般應該分配給一個能夠代表整個係統的類,這樣的類通常被命名為“XX處理器”、“XX協調器”或者“XX會話”。
關於控製器類,有如下原則:
a. 係統事件的接收與處理通常由一個高級類來代替。
b. 一個子係統會有很多控製器類,分別處理不同的事務。
6. Polymorphism
(多態)
這裏的多態跟OO三大基本特征之一的“多態”是一個意思。
例如:我們想設計一個繪圖程序,要支持可以畫不同類型的圖形,我們定義一個抽象類Shape,矩形(Rectangle)、圓形(Round)分別繼承這個抽象類,並重寫(override)Shape類裏的Draw()方法,這樣我們就可以使用同樣的接口(Shape抽象類)繪製出不同的圖形,如下圖:

7. Pure
Fabrication (純虛構)
這裏的純虛構跟我們常說的純虛構函數意思相近。高內聚低耦合,是係統設計的終極目標,但是內聚和耦合永遠都是矛盾對立的。高內聚以為這拆分出更多數量的類,但是對象之間需要協作來完成任務,這又造成了高耦合,反過來毅然。該如何解決這個矛盾呢,這個時候就需要純虛構模式,由一個純虛構的類來協調內聚和耦合,可以在一定程度上解決上述問題。
例如:上麵多態模式的例子,如果我們的繪圖程序需要支持不同的係統,那麼因為不同係統的API結構不同,繪圖功能也需要不同的實現方式,那麼該如何設計更合適呢?如下圖:
8. Indirection
(間接)
“間接”顧名思義,就是這個事不能直接來辦,需要繞個彎才行。繞個彎的好處就是,本來直接會連接在一起的對象彼此隔離開了,一個的變動不會影響另一個。就想我在前麵的低耦合模式裏說的一樣,“兩個不同模塊的內部類之間不能直接連接”,但是我們可以通過中間類來間接連接兩個不同的模塊,這樣對於這兩個模塊來說,他們之間仍然是沒有耦合/依賴關係的。
9. Protected Variations (受保護變化)
預先找出不穩定的變化點,使用統一的接口封裝起來,如果未來發生變化的時候,可以通過接口擴展新的功能,而不需要去修改原來舊的實現。也可以把這個模式理解為OCP(開閉原則)原則,就是說一個軟件實體應當對擴展開發,對修改關閉。在設計一個模塊的時候,要保證這個模塊可以在不需要被修改的前提下可以得到擴展。這樣做的好處就是通過擴展給係統提供了新的職責,以滿足新的需求,同時又沒有改變係統原來的功能。關於OCP原則,後麵還會有單獨的論述。
設計原則
1. 單一職責原則(SRP)
關於單一職責原則,其核心的思想是:一個類,隻做一件事,並把這件事做好,其隻有一個引起它變化的原因。單一職責原則可以看作是低耦合、高內聚在麵向對象原則上的引申,將職責定義為引起變化的原因,以提高內聚性來減少引起變化的原因。職責過多,可能引起它變化的原因就越多,這將導致職責依賴,相互之間就產生影響,從而極大的損傷其內聚性和耦合度。單一職責,通常意味著單一的功能,因此不要為一個模塊實現過多的功能點,以保證實體隻有一個引起它變化的原因。
- Unix/Linux是這一原則的完美體現者。各個程序都獨立負責一個單一的事。
- Windows是這一原則的反麵示例。幾乎所有的程序都交織耦合在一起。
2. 開放—封閉原則(OCP)
關於開放封閉原則,其核心的思想是:模塊是可擴展的,而不可修改的。也就是說,對擴展是開放的,而對修改是封閉的。
- 對擴展開放,意味著有新的需求或變化時,可以對現有代碼進行擴展,以適應新的情況。
- 對修改封閉,意味著類一旦設計完成,就可以獨立完成其工作,而不要對類進行任何修改。
對於麵向對象來說,需要你依賴抽象,而不是實現,23個經典設計模式中的“策略模式”就是這個實現。對於非麵向對象編程,一些API需要你傳入一個你可以擴展的函數,比如我們的C 語言的qsort()允許你提供一個“比較器”,STL中的容器類的內存分配,ACE中的多線程的各種鎖。對於軟件方麵,瀏覽器的各種插件屬於這個原則的實踐。
3. 依賴倒置原則(DIP)
高層模塊不應該依賴於低層模塊的實現,而是依賴於高層抽象。
舉個例子,牆麵的開關不應該依賴於電燈的開關實現,而是應該依賴於一個抽象的開關的標準接口,這樣,當我們擴展程序的時候,我們的開關同樣可以控製其它不同的燈,甚至不同的電器。也就是說,電燈和其它電器繼承並實現我們的標準開關接口,而我們的開關產商就可不需要關於其要控製什麼樣的設備,隻需要關心那個標準的開關標準。這就是依賴倒置原則。
這就好像瀏覽器並不依賴於後麵的web服務器,其隻依賴於HTTP協議。這個原則實在是太重要了,社會的分工化,標準化都是這個設計原則的體現。
4. 接口隔離原則(ISP)
接口隔離原則意思是把功能實現在接口中,而不是類中,使用多個專門的接口比使用單一的總接口要好。
舉個例子,我們對電腦有不同的使用方式,比如:寫作,通訊,看電影,打遊戲,上網,編程,計算,數據等,如果我們把這些功能都聲明在電腦的抽類裏麵,那麼,我們的上網本,PC機,服務器,筆記本的實現類都要實現所有的這些接口,這就顯得太複雜了。所以,我們可以把其這些功能接口隔離開來,比如:工作學習接口,編程開發接口,上網娛樂接口,計算和數據服務接口,這樣,我們的不同功能的電腦就可以有所選擇地繼承這些接口。
這個原則可以提升我們“搭積木式”的軟件開發。對於設計來說,Java中的各種Event Listener和Adapter,對於軟件開發來說,不同的用戶權限有不同的功能,不同的版本有不同的功能,都是這個原則的應用。
5. 替換原則(LSP)
軟件工程大師Robert C. Martin把裏氏代換原則最終簡化為一句話:“Subtypes must be substitutable for their base types”。也就是,子類必須能夠替換成它們的基類。即:子類應該可以替換任何基類能夠出現的地方,並且經過替換以後,代碼還能正常工作。另外,不應該在代碼中出現if/else之類對子類類型進行判斷的條件。裏氏替換原則LSP是使代碼符合開閉原則的一個重要保證。正是由於子類型的可替換性才使得父類型的模塊在無需修改的情況下就可以擴展。
這麼說來,似乎有點教條化,我非常建議大家看看這個原則個兩個最經典的案例——“正方形不是長方形”和“鴕鳥不是鳥”。通過這兩個案例,你會明白《墨子 小取》中說的 ——“娣,美人也,愛娣,非愛美人也….盜,人也;惡盜,非惡人也。”——妹妹雖然是美人,但喜歡妹妹並不代表喜歡美人。盜賊是人,但討厭盜賊也並不代表就討厭人類。這個原則讓你考慮的不是語義上對象的間的關係,而是實際需求的環境。
在很多情況下,在設計初期我們類之間的關係不是很明確,LSP則給了我們一個判斷和設計類之間關係的基準:需不需要繼承,以及怎樣設計繼承關係。
最後更新:2017-04-04 07:03:51