單一責任原則。 並不像看起來那麼簡單

單一責任原則。 並不像看起來那麼簡單 單一責任原則,又稱單一責任原則,
又名統一可變性原則 - 這是一個非常難以理解的傢伙,也是程式設計師面試中如此緊張的問題。

我第一次認真認識這個原理是在第一學年伊始,當時幼小的和綠色的幼蟲被帶到森林裡,使幼蟲成為學生——真正的學生。

在森林裡,我們被分成8-9人一組,進行了一場比賽——哪組人能最快喝完一瓶伏特加,前提是該組中第一個人將伏特加倒入玻璃杯中,第二個人喝掉,第三個有零食。 已完成操作的單元移至群組佇列的末端。

隊列大小為三的倍數的情況是 SRP 的良好實現。

定義 1. 單一責任。

單一責任原則(SRP)的官方定義指出,每個實體都有自己的責任和存在的理由,而且它只有一種責任。

考慮物件“Drinker”(酒鬼).
為了落實SRP原則,我們將職責分為三個部分:

  • 一倒(澆注操作)
  • 一杯飲料(飲酒行動)
  • 一個人吃點零食(咬住行動)

該流程中的每個參與者都負責該流程的一個組成部分,即具有一個原子責任 - 喝水、倒酒或吃零食。

反過來,飲水孔是這些操作的門面:

сlass Tippler {
    //...
    void Act(){
        _pourOperation.Do() // налить
        _drinkUpOperation.Do() // выпить
        _takeBiteOperation.Do() // закусить
    }
}

單一責任原則。 並不像看起來那麼簡單

為什麼呢?

人類程式設計師為猿人編寫程式碼,而猿人不專心、愚蠢且總是匆忙。 他可以一次掌握並理解大約 3 - 7 個術語。
對於醉漢來說,有以下三個術語。 然而,如果我們用一張紙來寫程式碼,那麼它就會包含手、眼鏡、打架和無休止的政治爭論。 所有這些都將在一個方法的主體中。 我相信您在實踐中見過這樣的程式碼。 這不是最人道的心理測驗。

另一方面,猿人被設計為在他的頭腦中模擬現實世界的物體。 在他的想像中,他可以將它們推在一起,組裝新的物體,並以相同的方式拆卸它們。 想像一輛舊模型汽車。 在你的想像中,你可以打開車門,扭開門飾,看到車窗升降機構,裡面有齒輪。 但您無法在一個「清單」中同時看到機器的所有組件。 至少「猴人」不能。

因此,人類程式設計師將複雜的機制分解為一組較不複雜的工作元素。 然而,它可以透過不同的方式分解:在許多舊汽車中,空氣管道進入車門,而在現代汽車中,電子鎖故障會導致引擎無法啟動,這可能是維修過程中的一個問題。

現在, SRP是一個原則,解釋瞭如何分解,即在哪裡劃分界線.

他說,要按照「責任」劃分的原則,即按照某些對象的任務來分解。

單一責任原則。 並不像看起來那麼簡單

讓我們回到飲酒以及猴人在分解過程中所獲得的好處:

  • 程式碼在各個層面都變得極為清晰
  • 該程式碼可以由多個程式設計師同時編寫(每個程式設計師編寫一個單獨的元素)
  • 自動化測試被簡化-元素越簡單,測試就越容易
  • 程式碼的組合性出現 - 您可以替換 飲酒行動 酒鬼將液體倒在桌子底下的行動。 或用混合葡萄酒和水或伏特加和啤酒的操作來代替傾倒操作。 根據業務需求,無需觸及方法程式碼即可完成所有操作 酒鬼法案.
  • 透過這些操作,您可以折疊貪食者(僅使用 採取比特操作)、酒精(僅使用 飲酒行動 直接從瓶子裡取出)並滿足許多其他業務要求。

(哦,看來這已經是OCP原則了,我違反了這個帖子的責任)

當然,還有缺點:

  • 我們必須創建更多類型。
  • 醉漢第一次喝酒比他原本應該喝酒的時間晚幾個小時。

定義 2. 統一變異性。

請允許我,先生們! 飲酒類也有單一的職責──它喝酒! 總的來說,「責任」這個詞是一個極其模糊的概念。 有人要對人類的命運負責,有人要對養育在極點翻倒的企鵝負責。

讓我們考慮一下飲水器的兩種實作。 上面提到的第一個包含三個類別 - 倒酒、飲料和小吃。

第二個是透過「Forward and Only Forward」方法編寫的,包含該方法中的所有邏輯 法案:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
   //...
   void Act(){
        // наливаем
    if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
        throw new OverdrunkException();

    // выпиваем
    if(!_hand.TryDrink(from: _glass,  size: _glass.Capacity))
        throw new OverdrunkException();

    //Закусываем
    for(int i = 0; i< 3; i++){
        var food = _foodStore.TakeOrDefault();
        if(food==null)
            throw new FoodIsOverException();

        _hand.TryEat(food);
    }
   }
}

從外部觀察者的角度來看,這兩個階級看起來完全一樣,並且承擔著相同的「飲酒」責任。

困惑!

然後我們上網查了SRP的另一個定義──單一可變性原則。

SCP 指出“一個模組有且僅有一個改變的理由」。 也就是說,「責任是改變的理由」。

(看來提出最初定義的傢伙對猿人的心靈感應能力很有信心)

現在一切都已就緒。 單獨地,我們可以改變倒酒、飲用和吃零食的程序,但在飲酒器本身中,我們只能改變操作的順序和組成,例如,在飲用前移動零食或添加吐司的閱讀。

在「向前且僅向前」的方法中,所有可以改變的東西都只在方法中改變 法案。 當邏輯很少且很少改變時,這可能是可讀且有效的,但通常會以每行 500 行的可怕方法結束,其中的 if 語句比俄羅斯加入北約所需的還要多。

定義 3. 變更的在地化。

飲酒者常常不明白為什麼他們在別人的公寓裡醒來,或是他們的手機在哪裡。 是時候新增詳細的日誌記錄了。

讓我們開始記錄澆注過程:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Before pour with {_hand} and {_bottle}");
        //Pour business logic ...
        _log.Log($"After pour with {_hand} and {_bottle}");
    }
}

透過將其封裝在 澆注操作,我們從責任和封裝的角度採取了明智的行動,但現在我們對可變性原則感到困惑。 除了操作本身可以改變之外,日誌記錄本身也可以改變。 您必須為澆注操作分離並建立一個特殊的記錄器:

interface IPourLogger{
    void LogBefore(IHand, IBottle){}
    void LogAfter(IHand, IBottle){}
    void OnError(IHand, IBottle, Exception){}
}

class PourOperation: IOperation{
    PourOperation(IPourLogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.LogBefore(_hand, _bottle);
        try{
             //... business logic
             _log.LogAfter(_hand, _bottle");
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

細心的讀者會注意到 日誌後, 之前記錄 и 錯誤時 也可以單獨更改,類比前面的步驟,會建立三個類別: 倒入記錄器之前, 倒入記錄器之後 и 澆注錯誤記錄器.

記住飲酒者有 14 個操作,我們有 XNUMX 個記錄類。 結果,整個飲酒圈由XNUMX(!!!)個等級組成。

雙曲線? 幾乎不! 一個拿著分解手榴彈的猴人將把「倒酒器」分成一個醒酒器、一個玻璃杯、倒酒操作員、一個供水服務、一個分子碰撞的物理模型,在下個季度,他將嘗試在不全局變數。 相信我,他不會停止。

正是在這一點上,許多人得出結論,SRP是來自粉紅王國的童話故事,走開去玩麵條...

……沒有了解 Srp 第三個定義的存在:

「單一責任原則指出 類似變化的東西應該​​存放在一個地方」。 或者 ”一起更改的內容應保留在一個地方

也就是說,如果我們更改操作的日誌記錄,那麼我們必須在一個地方更改它。

這是非常重要的一點——因為上面所有對SRP的解釋都說需要在粉碎類型的同時粉碎它們,也就是說,他們對對象的大小施加了“上限”,而現在我們已經在談論“下限”了。 換句話說, SRP不僅要求“壓碎時壓碎”,而且也不能過度——“不要壓碎環環相扣的東西”。 這就是奧卡姆剃刀與猿人之間的偉大之戰!

單一責任原則。 並不像看起來那麼簡單

現在飲酒者應該會感覺好多了。 除了不需要將 IPourLogger 記錄器分為三個類別之外,我們還可以將所有記錄器合併為一種類型:

class OperationLogger{
    public OperationLogger(string operationName){/*..*/}
    public void LogBefore(object[] args){/*...*/}       
    public void LogAfter(object[] args){/*..*/}
    public void LogError(object[] args, exception e){/*..*/}
}

如果我們新增第四種操作,那麼它的日誌記錄就已經準備好了。 並且操作本身的程式碼是乾淨的並且沒有基礎設施噪音。

因此,我們有 5 個課程來解決飲酒問題:

  • 澆注作業
  • 飲酒操作
  • 幹擾操作
  • 記錄器
  • 飲水器立面

他們每個人都嚴格負責一項功能,並且有一個變更原因。 所有與更改類似的規則都位於附近。

現實生活中的例子

我們曾經編寫過一個自動註冊 B2B 用戶端的服務。 對於200行類似的內容,出現了一個GOD方法:

  • 前往1C並建立帳戶
  • 使用此帳戶,前往支付模組並在那裡創建它
  • 檢查主伺服器上沒有建立該帳戶的帳戶
  • 建立一個新帳戶
  • 將支付模組中的註冊結果和1c號加入註冊結果服務中
  • 將帳戶資訊新增至此表中
  • 在積分服務中為此用戶端建立一個積分編號。 將您的 1c 帳號傳遞給此服務。

這份名單上還有大約 10 個連結性很差的企業。 幾乎每個人都需要帳戶物件。 一半的通話需要點 ID 和客戶端名稱。

經過一個小時的重構,我們能夠將基礎設施程式碼和使用帳戶的一些細微差別分離到單獨的方法/類別中。 上帝的方法讓事情變得更簡單,但剩下的 100 行程式碼就是不想理清。

幾天後才明白,這種「輕量級」方法的本質就是商業演算法。 而且技術規格的原始描述相當複雜。 嘗試將此方法分解為多個部分就會違反 SRP,反之亦然。

形式主義。

是時候讓我們的醉漢一個人呆著了。 擦乾你的眼淚-總有一天我們一定會回來的。 現在讓我們形式化本文中的知識。

形式主義一、SRP的定義

  1. 將元素分開,使每個元素負責一件事。
  2. 責任代表「改變的理由」。 也就是說,就業務邏輯而言,每個元素只有一個更改原因。
  3. 業務邏輯的潛在變化。 必須本地化。 同步變更的元素必須位於附近。

形式主義2.必要的自我檢測標準。

我還沒有看到滿足 SRP 的足夠標準。 但有必要條件:

1)問問自己這個類別/方法/模組/服務的作用。 你必須用一個簡單的定義來回答它。 ( 謝謝 布萊托里 )

解釋

然而,有時很難找到一個簡單的定義

2) 修復錯誤或新增功能會影響最小數量的檔案/類別。 理想情況下 - 一個。

解釋

由於責任(針對某個功能或錯誤)被封裝在一個文件/類別中,因此您確切地知道在哪裡查看以及編輯什麼內容。 例如:更改日誌記錄操作的輸出的功能將只需要更改記錄器。 無需運行其餘程式碼。

另一個範例是新增新的 UI 控件,與先前的控件類似。 如果這迫使您添加 10 個不同的實體和 15 個不同的轉換器,那麼看起來您做得太過分了。

3)如果多個開發人員正在開發專案的不同功能,那麼發生合併衝突的可能性(即多個開發人員同時更改相同文件/類別的可能性)是最小的。

解釋

如果在增加一個新操作「在桌子底下倒伏特加」時,需要影響記錄器,即喝和倒酒的操作,那麼看起來職責劃分歪了。 當然,這並不總是可能的,但我們應該盡力減少這個數字。

4)當(來自開發人員或經理)詢問有關業務邏輯的澄清問題時,您嚴格進入一個類別/文件並僅從那裡接收資訊。

解釋

特徵、規則或演算法都被緊湊地編寫在一個地方,並且不會在整個程式碼空間中散佈著標誌。

5)命名清晰。

解釋

我們的類別或方法負責一件事,而責任就體現在它的名字中

AllManagersManagerService - 很可能是上帝類
LocalPayment - 可能不是

形式主義3.奧卡姆優先發展方法論。

在設計之初,猴人不知道也感受不到所解決問題的所有微妙之處,並且可能會犯錯。 你可能會以不同的方式犯錯:

  • 透過合併不同的職責使物件變得太大
  • 透過將單一職責劃分為許多不同類型來重新建構
  • 錯誤界定責任邊界

記住這條規則很重要:“最好犯一個大錯誤”,或者“如果你不確定,就不要把它分開”。 例如,如果您的類別包含兩個職責,那麼它仍然是可以理解的,並且可以在對客戶端程式碼進行最小更改的情況下將其拆分為兩個。 由於上下文分佈在多個文件中且客戶端程式碼中缺乏必要的依賴關係,因此用玻璃碎片組裝玻璃通常更加困難。

是時候到此為止了

SRP的範圍不限於OOP和SOLID。 它適用於方法、函數、類別、模組、微服務和服務。 它適用於“figax-figax-and-prod”和“火箭科學”開發,讓世界各地變得更美好。 如果你仔細想想,這幾乎是所有工程的基本原則。 機械工程、控制系統以及事實上所有複雜的系統都是由組件構建的,“碎片化不足”剝奪了設計者的靈活性,“過度碎片化”剝奪了設計者的效率,不正確的邊界剝奪了他們的理性和內心的平靜。

單一責任原則。 並不像看起來那麼簡單

SRP 不是自然發明的,也不屬於精確科學的一部分。 它突破了我們生物和心理的限制,只是利用猿人大腦來控制和發展複雜系統的一種方式。 他告訴我們如何分解一個系統。 最初的表述需要大量的心靈感應,但我希望這篇文章能夠消除一些煙幕彈。

來源: www.habr.com

添加評論