1. 程式人生 > >GRASP----(職責分配原則)

GRASP----(職責分配原則)

要學習設計模式,有些基礎知識是我們必須要先知道的,設計模式是關於類和物件的一種高效、靈活的使用方式,也就是說,必須先有類和物件,才能有設計模式的用武之地,否則一切都是空談,那麼類和物件是從那冒出來的呢?這時就需要比23種設計模式更重要更經典的GRASP模式登場了。

GRASP(General Responsibility Assignment Software Patterns),中文名稱為“通用職責分配軟體模式”,GRASP一共包括9種模式,如何決定一個系統有多少物件,每個物件都包括什麼職責,GRASP模式給出了最基本的指導原則。初學者應該儘快掌握、理解這些原則,因為這是如何設計一個面向物件系統的基礎。可以說,GRASP是學習使用設計模式的基礎。

1、Infomation Expert(資訊專家)

資訊專家模式是面向設計的最基本原則,是我們平時使用最多,應該跟我們的思想融為一體的原則。也就是說,我們設計物件(類)的時候,如果某個類擁有完成某個職責所需要的所有資訊,那麼這個職責就應該分配給這個類來實現。這時,這個類就是相對於這個職責的資訊專家。
例如: 常見的網上商店的購物車(ShopCar),需要讓每種商品(SKU)只在購物車內出現一次,購買相同商品,只需要更新商品的數量即可。如下圖:
這裡寫圖片描述

針對這個問題需要權衡的是,比較商品是否相同的方法需要放到哪個類裡來實現呢?分析業務得知需要根據商品的編號(SKUID)來唯一區分商品,而商品編號是唯一存在於商品類的,所以根據資訊專家模式,應該把比較商品是否相同的方法放在商品類裡。

2、Creator(創造者)

實際應用中,符合下列任一條件的時候,都應該由類 A 來建立類 B,這時 A 是 B 的建立者:
a、A 是 B 的聚合
b、A 是 B 的容器
c、A 持有初始化 B 的資訊(資料)
d、A 記錄 B 的例項
e、A 頻繁使用 B

如果一個類建立了另外一個類,那麼這兩個類之間就有了耦合,也可以說產生了依賴關係。依賴或耦合本身是沒有錯誤的,但是他們帶來的問題就是在以後的維護中產生連鎖反應,而必要的耦合是逃不掉的,我們能做的就是正確的建立耦合關係,不要隨便建立類之間的依賴關係,那麼該如何去做呢?就是要遵守建立者模式規定的基本原則,凡是不符合以上條件的,都不能隨便用 A 建立 B。

例如:因為訂單(Order)是商品(SKU)的容器,所以應該由訂單來建立商品。如下圖:
這裡寫圖片描述

這裡因為訂單是商品的容器,也只有訂單持有初始化商品的資訊,所以這個耦合關係是正確的且沒有辦法避免的,所以由訂單來建立商品。

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抽象類)繪製出不同的圖形,如下圖:
這裡寫圖片描述

這樣的設計更符合高內聚和低耦合原則,雖然後來我們又增加了一個菱形(Diamond)類,對整個系統結構也沒有任何影響,只要增加一個繼承 Shape 類就行了。

7、Pure Fabrication(純虛構)

這裡的純虛構跟我們常說說的純虛構函式意思相近。高內聚低耦合,是系統設計的終極目標,但是內聚和耦合永遠都是矛盾對立的。高內聚以為這拆分出更多數量的類,但是物件之間需要協作來完成任務,這又造成了高耦合,反過來依然。該如何解決這個矛盾呢?這個時候就需要純虛構模式,由一個純虛構的類來協調內聚和耦合,可以在一定程度上解決上述問題。

例如:上面多型模式的例子,如果我們的繪圖程式需要支援不同的系統,那麼因為不同系統的API結構不同,繪圖功能也需要不同的實現方式,那麼該如何設計更合適呢?如下圖:
這裡寫圖片描述

這裡我們可以看到,因為增加了純虛構類AbstractShape,不論是哪個系統都可以通過AbstractShape 類來繪製圖形,我們即沒有降低原來的內聚性,也沒有增加過多的耦合,可謂魚肉和熊掌兼得。

8、Indirection(間接)

“間接”顧名思義,就是這個事不能直接來辦,需要繞個彎才行。繞個彎的好處就是,本來直接會連線在一起的物件彼此隔離開了,一個變動不會影響另一個。就像我在前面的低耦合模式裡說的一樣,“兩個不同模組的內部類之間不能直接連線”,但是我們可以通過中間類來間接連線兩個不同的模組,這樣對於這兩個模組來說,他們之間仍然是沒有耦合/依賴關係的。

9、Protected Variations(受保護變化)

預先找出不穩定的變化點,使用統一的介面封裝起來,如果未來發生變化的時候,可以通過介面擴充套件新的功能,而不需要去修改原來舊的實現。也可以把這個模式理解為 OCP(開閉原則),就是說一個軟體實體應當對拓展開發,對修改關閉。在設計一個模組的時候,要保證這個模組可以在不需要被修改的前提下可以得到拓展。這樣做的好處就是通過拓展給系統提供了新的職責,以滿足新的需求,同時又沒有改變系統原來的功能。

比設計模式更重要:設計原則

我們生活在一個充滿規則的世界裡,在複雜多變的外表下,萬事萬物都被永恆的真理支配並有規律的執行著。模式也是一樣,不論那種模式,其背後都潛藏著一些“永恆的真理”,這個真理就是設計原則。記得一次參加微軟的架構師培訓,期間講到設計模式,有人問了老師一個問題:“什麼東西比設計模式更重要?”,老師是一位有多年豐富實踐經驗的開發者,他毫不猶豫地回答到:“比模式更重要的是原則”。這句話我時常能夠想起,越來越覺得這是一個偉大的答案。的確,還有什麼比原則更重要呢?就像人的世界觀和人生觀一樣,那才是支配你一切行為的根本,而對於設計模式來說,為什麼這個模式要這樣解決這個問題,而另一個模式要那樣,它們背後都遵循的就是永恆的設計原則。可以說,設計原則是設計模式的靈魂。

對於設計原則的深入探討我還沒有那個深度,推薦大家去看《敏捷軟體開發—原則、模式與實踐》,下面僅對部分常用的設計原則做些簡單的講解:

1、單一職責原則(SRP)

“就一個類而言,應該僅有一個引起它變化的原因。”也就是說,不要把變化原因各不相同的職責放在一起,因為不同的變化會影響到不相干的職責。再通俗一點地說就是,不該你管的事情你不要管,管好自己的事情就可以了,多管閒事害了自己也害了別人。(當然這裡說的多管閒事跟見義勇為是兩回事,我們提倡見義勇為!)

例如:參考下圖中的設計,圖形計算程式只使用了正方形的Area()方法,永遠不會使用Draw()方法,而它卻跟Draw方法關聯了起來。這違反了單一原則,如果未來因為圖形繪製程式導致Draw()方法產生了變化,那麼就會影響到本來毫不關係的圖形計算程式。
這裡寫圖片描述

那麼我們該怎麼做呢?如下圖,將不同的職責分配給不同的類,使單個類的職責儘量單一,就隔離了變化,這樣他們也不會互相影響了。
這裡寫圖片描述

2、開放——封閉原則(OCP)

“軟體實體(類、模組、函式等)應該是可以擴充套件的,但是不可修改。”嘿!多麼樸實的話語,第一次看這個原則的時候我都看傻了,我當時在想“這不是&#%做白日夢嗎!不修改怎麼擴充套件啊?”但是隨著學習的深入,理解了這個“不修改”是什麼意思,意思是“你可以隨便增加新的類,但是不要修改原來的類”。從這個角度去理解就好多了,其實這裡還是一個隔離變化的問題。

例如:如下圖,有一個客戶端程式通過資料訪問介面操作資料,對於這套系統來說,一開始計劃使用的是SQL Server或Oracle資料庫,但是後來考慮到成本,改用免費的mysql;那麼對於客戶端程式來說,後來資料的擴充套件對它沒有任何影響,它在不知不覺間就用上了免費好用的MySQL資料庫,這全要感謝OCP原則。
這裡寫圖片描述

3、依賴倒置原則(DIP)

“抽象不應該依賴於細節。細節應該依賴於抽象。”關於這個原則,還有種說法是.“高層不應該依賴於底層,兩者都應該依賴於抽象。”其實怎麼說都是對的,關鍵就是要理解一點,只有抽象的東西才是最穩定的,也就是說,我們依賴的是它的穩定。如果將來“抽象”也不穩定了,那麼誰穩定我跟誰,其實說白了不就是傍大款嗎!

例如:參考下圖的設計,一個開關跟燈直接連線在一起了,也就是說開關依賴於燈的開啟和關閉方法,那麼如果我想用這個開關也可以開啟其他東西呢,比如電視、音響。顯然這個設計是無法滿足這個要了,因為我們依賴了細節而不是抽象,這個開關已經等價於“燈的開關”。
這裡寫圖片描述

那麼我們該如何來設計一個通用的開關呢?參考下圖的設計,OK!現在我們不僅可以開啟燈,還可以開啟電視和音響,甚至未來任何實現了“開關介面”的任何東西。
這裡寫圖片描述

4、介面隔離原則(ISP)

“不應該強迫客戶依賴於它們不用的方法。介面屬於客戶,不屬於它所在的類層次結構。”這個說得很明白了,再通俗點說,不要強迫客戶使用它們不用的方法,如果強迫使用者使用它們不使用的方法,那麼這些客戶就會面臨由於這些不使用的方法的改變所帶來的改變。

例如:參考下圖的設計,在這個設計裡,取款、存款、轉帳都使用一個通用介面介面,也就是說,每一個類都被強迫依賴了另兩個類的介面方法,那麼每個類有可能因為另外兩個類的方法(跟自己無關)而被影響。拿取款來說,它根本不關心“存款操作”和“轉帳操作”,可是它卻要受到這兩個方法的變化的影響,真是土鱉!
這裡寫圖片描述

那麼我們該如何解決這個問題呢?參考下圖的設計,為每個類都單獨設計專門的操作介面,使得它們只依賴於它們關係的方法,這樣就不會互相影響,也就不會在發生土鱉的事情了!
這裡寫圖片描述

5、替換原則(LSP)

“子型別必須能夠替換掉它們的基型別。”也就是說繼承中的“IS A”關係是必須保證的,否則還算什麼繼承啊!如果違反了LSP原則,常會導致在執行時(RTTI)的型別判斷違反OCP原則。

例如:函式A的引數是基型別,呼叫時傳遞的物件是子型別,正常情況下,增加子型別都不會影響到函式A的,如果違反了LSP,則函式A必須小心的判斷傳進來的具體型別,否則就會出錯,這就已經違反了OCP原則。

關於模式學習

深刻理解面向物件是學好設計模式的基礎,掌握一定的面向物件設計原則才能掌握面向物件設計模式的精髓,從而實現靈活運用設計模式。僅知道OO的語言機制是不夠的,懂得語言裡的封裝、繼承、多型,只是滿足了最最基礎的條件,要真正發揮OO的強大的作用,關鍵是要深刻理解以上的GRASP模式和設計原則,在此基礎上去再深入理解設計模式,並在實踐中不斷磨練。

模式跟OO原則相比其實並不重要,如果你能設計出基本符合以上原則的程式,那麼可能就已經總結出了新的模式,所以學習模式的根本是為了深入理解OO思想和原則,使我們可以寫出高內聚低耦合的程式。

另外最近在學習李建忠老師的“C#面向物件設計模式縱橫談系列課程”時候,李老師提出了一個“重構到模式”的理論,感覺十分有道理,模式不完全是供我們套用的模版,在特定的業務環境下,我們實現的可能只是“類似XX模式”的設計模式,因為針對這個環境,這麼使用就是最合適的,而不是什麼時候都必須完全照搬GOF的23種設計模式的格式,模式是死的,而人是活的,找到最合適的實現方式就好,不要為了設計模式而使用設計模式。