設計模式六大原則例子(二)-- 單一職責原則(SRP)例子
之前我們對設計模式的六大原則做了簡單歸納,這篇部落格是對單一職責原則進行的舉例說明。
單一職責原則的意義
物件不應該承擔太多職責,正如人不應該一心分為二用。唯有專注,才能保證物件的高內聚;唯有單一,才能保證物件的細粒度。物件的高內聚與細粒度有利於物件的重用。一個龐大的物件承擔了太多的職責,當客戶端需要該物件的某一個職責時,就不得不將所有的職責都包含進來,從而造成冗餘程式碼或程式碼的浪費。這實際上保證了DRY原則,即”不要重複你自己(Don’t Repeat Yourself)”,確保系統中的每項知識或功能都只在一個地方描述或實現。
單一職責原則並不是極端地要求我們只能為物件定義一個職責,而是利用極端的表述方式重點強調:
1. 在定義物件職責時,必須考慮職責與物件之間的所屬關係。
2. 職責必須恰如其分地表現物件的行為,而不至於破壞和諧與平衡的美感,甚至格格不入。換言之,該原則描述的單一職責指的是公開在外的與該物件緊密相關的一組職責。
單一職責原則的優點
單一職責原則還有利於物件的穩定(所謂”職責”,就是物件能夠承擔的責任,並以某種行為方式來執行)。物件的職責總是要提供給其他物件呼叫,從而形成物件與物件的協作,由此產生物件之間的依賴關係。物件的職責越少,則物件之間的依賴關係就越少。耦合度減弱,受其他物件的約束與牽制就越少,從而保證了系統的可擴充套件性。
- 類的複雜性降低,實現什麼職責都有清晰明確的定義
- 可讀性提高,複雜性降低,那當然可讀性提高了
- 可維護性提高,那當然了,可讀性提高,那當然更容易維護了
- 變更引起的風險降低,變更是必不可少的,介面的單一職責做的好的話,一個介面修改只對相應的實現類有影響
單一職責與介面隔離的區別:
- 單一職責原則注重的是職責;而介面隔離原則注重對介面依賴的隔離。
- 單一職責原則主要是約束類,其次才是介面和方法,它針對的是程式中的實現和細節; 而介面隔離原則主要約束介面,主要針對抽象,針對程式整體框架的構建。
單一職責原則的例子
1.行為與行為分離開來:
舉個電話的例子,我們使用電話的時候有4個過程發生:拔號、說話、聽別人說、掛機,我們將這四個功能寫在一個接口裡,其類圖如下:
程式碼如下:
iPhone這些介面包含了兩個職責:一個是協議管理,由dial()與 hangup()兩個方法實現;一個是資料傳輸,由chat()與 answer()實現。其中資料傳輸部分chat()和 answer()會使用到協議管理的方法。
1.協議接通的變化(dial()與 hangup())會引起這個介面或實現類的變化嗎? 會的!
資料傳輸(電話不僅僅通話,還可以上網)的變化也會引起其變化!這兩個原因都引起了類的變化。
2.電話拔通還用管使用什麼協議嗎?電話連線後還需要關心傳遞什麼資料嗎? 都不會,即這兩個職責的變化不相互影響,那就考慮拆分成兩個介面,類圖如下:
雖然這個類圖略有些複雜,一個手機類需要把兩個物件組合在一起才能用,但是因為“資料傳輸”需要用到“連結管理”的功能。所以這些犧牲是合適的(推薦使用)。如果我們用介面隔離的原則去實現:會有如下的結果,弊端是會將連結管理的操作和資料傳輸全部耦合到iPhone中去(iPhone中會寫很多的業務邏輯程式碼去進行我們對 連結管理+資料傳輸 的操作),不推薦
2.職責分明到介面:
當我們要修改使用者的資訊的時候:對應使用者的各種資訊我們可以選擇每個方法對單一屬性進行修改,而不是在一個接口裡更改所有的資訊:
職責不清晰,不單一,我們改成:
3.一個違反SRP的例子:
所謂職責擴散,就是因為某種原因,職責P被分化為粒度更細的職責P1和P2
比職:類T只負責一個職責(或功能)P,這樣設計是符合SRP的。後來由於某種原因,需要將職責P細分為粒度更細的P1與P2,這時如果要遵循SRP,需要將類T也分解為兩個類T1和T2,分別負責P1、P2這兩個職責。但是在程式已經寫好的情況下,這樣做簡直太費時間了。所以,簡單的修改類T,用它來負責兩個職責是一個比較不錯的選擇,雖然這樣做有悖於SRP。
如,用一個類描述動物呼吸這個場景:
執行結果:
程式上線後,發現問題了,並不所有動物都呼吸空氣的,如魚是呼吸水的。 修改時如若遵循SRP,則需將Animal類細分為陸生動物類Terrestrial ,水生動物 Aquatic,程式碼如下:[修改方式一]
我們發現這樣修改花銷是很大的,除了將原來的類分解之外,還需要修改客戶端。而直接修改類Animal來達成目的,雖然違背SRP,但花銷卻小的多,修改了原有程式碼,直接違反了SRP,不可這樣,程式碼如下:[修改方式二]
執行結果是:
可以看到,這種修改方式要簡單得多。但是”小金魚呼吸空氣“顯然是錯誤的。隱患:有一天需要將魚分為呼吸淡水和海水的魚,則又要修改Animal類的breathe方法,而對原有的程式碼修改會對呼叫“羊”等相關功能帶來風險,也許某一天,你會發現程式執行的結果變為“羊呼吸水”了。這種修改方式直接在程式碼級別上違背了SRP,雖然修改起來最簡單,但隱患卻最大。還有一種修改方式,在類級別違反了SRP,但是在方法級別遵守了SRP:[修改方式三]
執行結果:
可以看出,這種修改沒有改動原來的方法,而是在類中添加了一個方法,這樣雖然也違背了SRP,但在方法級別上卻是符合SRP的,因為它並沒有改動原來方法的程式碼。這三種方式各有優缺點,那麼在實際程式設計中,採用哪一種呢?這需要根據實際情況而定:建議:
A.只有邏輯足夠簡單,才可以在程式碼級別上違背SRP;
B.只有類中方法數量足夠少,才可以在方法級別上違背SRP;
例如本文所舉的這個例子,它太簡單,它只有一個方法,所以,無論在程式碼還是在方法級別違背SRP,都不會造成太大的影響。
實際應用中的類都要複雜的多,一旦發生職責擴散而需要修改類時,除非這個類本身非常簡單,否則還是要遵循SRP。