概述
“分離職責”是經常使用的一個重構策略,當一個類擔任的職責太多時,應按職責將它拆分成多個類,每個類分別承擔“單一”的職責,也就是讓每個類專心地做“一件事情”。
SRP原則
在面向對象編程中,SRP原則是一個非常重要的原則(SOLID原則都很重要),在展示示例前,我們先了解一下SRP原則是什么,以及它有什么作用。
什么是SRP原則?
SRP原則的定義是這樣的:
There should never be more than one reason for a class to change.
就一個類而言,應該僅有一個引起它變化的原因。
為什么要遵守SRP原則?
When a class has more than one responsibility, there are also more triggers and reasons to change that class. A responsibility is the same as “a reason for change” in this context.
因為每一個職責都是變化的因子,當需求變化時,該變化通常反映為類的職責的變化。
如果一個類承擔了多於一個的職責,那么就意味着引起它的變化的原因會有多個,等同於把這些職責耦合在了一起。
一個職責的變化可能會抑制到該類完成其他職責的能力,這樣的耦合會導致脆弱的設計。
在設計類或接口時,如果能夠遵守SRP原則,則會帶來以下優點:
- 類的復雜性降低:每個類或接口都只定義單一的職責,定義清晰明確
- 可讀性提高:定義清晰明確,自然帶來較高的代碼可讀性
- 可維護性提高:代碼可讀性提高,意味着更容易理解;單一職責使得類之間的耦合性較低,更改也會較為容易
- 擴展性更好:當需要擴展新的職責時,只需要定義新的接口和新的實現即可
如何遵守SRP原則?
Separating responsibility can be done by defining for every responsibility a class or an interface.
應該為每一項職責定義類或接口的方式實現職責分離。
SRP原則的難點
SRP原則堪稱是SOLID原則里面最簡單的一個原則,但也可以說是最難的一個。
它的“簡單”之處在於它很容易被理解,“困難”之處在於很多人在軟件設計過程中,很難真正地抓住關鍵點。
對於開發者來說,“分離職責”存在四個難點,也是開發者在使用這種重構策略時需要慎重考慮的地方
職責的划分
“職責”單一是相對的,每個人看事情的角度,對業務的理解程度是不盡相同的,這導致了人們對職責的定義和細化程度的差異性。同樣一個業務,有些人從角度A出發,在對業務提煉歸納總結后,得出三項職責:J、K、L。而有些人則從角度B出發,歸納總結出兩項職責:X、Y。
在設計接口時,這兩人自然而然地會設計出不同的接口,兩人設計的接口個數和表達的語義也各不相同。
拿裝修房子來說,業主通常會委托裝修公司來做這件事兒,站在業主的角度理解,它就是一件大事兒——“裝修公司裝修房子”,至於怎么裝修,由裝修公司來搞定。
裝修公司通常會將這件大事兒拆分成幾件小事兒,譬如:“室內設計”、“貼瓷磚”、“做家具”、“刷牆”等等,然后再去雇佣不同類型的工人來完成這些小事兒。
類的命名
職責和類的命名應該匹配,如果在職責歸納時,歸納出的職責比較模糊,可能會使類的命名變得艱難。
另外,即使你歸納出的職責是清晰的,如果命名與職責不符(詞不達意),仍然會給將來的維護、再重構帶來一些困難(命名是非常非常重要的)。
類粒度的控制
將多個職責被拆分到多個類時,原本在一個類中體現的職責被分散到多個類了,與此同時也需要考慮類的粒度。
粒度應當適中,粒度的控制沒有固定的標准,這需要結合業務場景具體分析。
類之間的依賴
原本用一個類就能完成的功能,現在需要結合多個類才能完成。
現在為了確保原有的功能仍然能正常運行,較大可能會形成多個類之間的依賴關系。
如果有些類被遷移到其他工程了,這還會涉及到工程之間的依賴關系。
小結
“分離職責”是比較難的一個重構策略,尤其是在一些大型項目中。
該策略如果不能良好地利用,可能會讓你的工程或解決方案變得不倫不類。
如果你的重構經驗較淺,建議你從一些較小的項目練習這項重構策略。
示例
重構前
這段代碼包含兩個類Video和Customer。
Viedo類包含三個職責:支付費用、租借Video和計算租金。
Video的職責太多,它把Customer類的職責也“搶”過來了。
public class Video { public void PayFee(decimal fee) { } public void RendVideo(Video video, Customer customer) { customer.Videos.Add(video); } public decimal CalculateBalance(Customer customer) { return customer.LateFees.Sum(); } } public class Customer { public IList<decimal> LateFees { get; set; } public IList<Video> Videos { get; set; } }
重構后
在歸納職責時,我們可以通過識別主語的方式來確定其歸屬。
- 支付費用的主語是“客戶”,即“客戶支付費用”。
- 計算租金的主語也是“客戶”,即“計算客戶的租金”。
所以,我們可以將Video類的PayFee()、CalculateBalance()方法放到Customer類中。
public class Video { public void RentVideo(Video video, Customer customer) { customer.Videos.Add(video); } } public class Customer { public IList<decimal> LateFees { get; set; } public IList<Video> Videos { get; set; } public void PayFee(decimal fee) { } public decimal CalculateBalance(Customer customer) { return customer.LateFees.Sum(); } }
原則就像基准線,在設計類和接口時,我們應該盡量遵守基准線,而不是死守基准線,在設計時不應死板地依照原則進行設計。這就好比開車,司機的視線應該始終保持在正前方,如果沿着公路上的線開車而忽視了前方的交通情況,可能會引發交通事故。