小酌重構系列[10]——分離職責


概述

“分離職責”是經常使用的一個重構策略,當一個類擔任的職責太多時,應按職責將它拆分成多個類,每個類分別承擔“單一”的職責,也就是讓每個類專心地做“一件事情”。

SRP原則

在面向對象編程中,SRP原則是一個非常重要的原則(SOLID原則都很重要),在展示示例前,我們先了解一下SRP原則是什么,以及它有什么作用。

image

什么是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原則,則會帶來以下優點:

  1. 類的復雜性降低:每個類或接口都只定義單一的職責,定義清晰明確
  2. 可讀性提高:定義清晰明確,自然帶來較高的代碼可讀性
  3. 可維護性提高:代碼可讀性提高,意味着更容易理解;單一職責使得類之間的耦合性較低,更改也會較為容易
  4. 擴展性更好:當需要擴展新的職責時,只需要定義新的接口和新的實現即可

如何遵守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();
    }
}

 

原則就像基准線,在設計類和接口時,我們應該盡量遵守基准線,而不是死守基准線,在設計時不應死板地依照原則進行設計。這就好比開車,司機的視線應該始終保持在正前方,如果沿着公路上的線開車而忽視了前方的交通情況,可能會引發交通事故。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM