使人瘋狂的 SOLID 原則:單一職責原則 (Single Responsibility Principle)

YC
程式愛好者
Published in
6 min readAug 24, 2020

--

今天我們要說的是第一個原則:單一職責原則(SRP)

嗯.. 這真的是一個是一個害死人的名稱,很多人會以為他意味每個模組都應該只做一件事。但真正的定義卻並非如此:

按照慣例,先上定義:“A class should have only one reason to change.”
翻譯成中文是:「一個模組應有且只有一個理由會使其改變。」

但是在 Clear Architecture 一書中,作者說到軟體系統改變是為了滿足使用者與利益相關者,所以我們應該把這理由套用到定義上,即變成:
「一個模組應只對唯一一個使用者或利益相關者負責。」

可是如果現在有多個使用者或利益相關者希望以相同的方式改變的話,我們不就會輕易地違反定義了嗎?

所以我們需要為一群希望以相同的方式改變的使用者或利益相關者一個定義,在這邊作者稱之為角色。

最終定義為:「一個模組應只對唯一的一個角色負責。」

對,疑問來了,角色是什麼?這樣做的好處是什麼?違反這原則會帶來什麼壞處?

為什麼原來的定義跟推導後得出的定義會差這麼多????????

所以,要了解單一職責原則我們需要一些例子、一點更具體的想法。

讓我們來看看第一個例子:
假如今天我們在做一個電商平台,而裡面分別有計算消費稅、信用卡手續費的 function,且同時應用在賣家後台與電商管理後台。

class feeCalculator {
public function calcTax();
public function calcCreditCardFee();
}

如果今天信用卡公司跟電商平台有了個協議,
信用卡公司:「每筆單我給貴公司減少 5% 的抽成!」
電商公司高興地想:「那我就可以維持對消費者來 10% 的抽成,自己賺那5%,爽翻天囉!」
然後馬上請工程帥去改程式。

故事的結果當然是工程帥去改同時面對賣家後台與電商後台的 calcCreditCardFee(),然後公司就是少賺了幾千萬,最後工程帥被炒了。Happy Ending!

這邊就可以回到「一個模組應只對唯一的一個角色負責。」的定義,今天因為工程師不知道原來這模組同時對兩個角色負責了 — — 賣家後台與電商管理後台,把不同角色所依賴的程式碼放在一起,他的改動同時造成兩個角色的行為改變,最後導致錯誤發生。

而 SRP 就是希望我們極力避免這種事情的發生,正是要限制改變帶來的影響力。

那上面的錯誤我們又該如何做一個更好的處理呢?
最簡單的想法就是為 calcCreditCardFee() 分別封裝起來,以應對不同的角色

class SalesFeeCalculator {      // for 賣家
public function calcTax();
public function calcCreditCardFee();
}
class AdminFeeCalculator { // for 後台
public function calcTax();
public function calcCreditCardFee();
}

OK,這樣我們 10 個角色用到很類似的計算 function,我們就要 10 個 class,然後 class 裡面的 function 都只有點一差異,這樣好像又有點怪怪的?

那如果是透過控制項來管理呢?

class FeeCalculator {private role = null;

public __construct(Role role) {
this.role = role
}

public function calcTax();
public function calcCreditCardFee() {
if (this.role == Sales) {
return ....;
}else if (this.role == Admin){
return ....;
}else{
return ....;
}
}
}

這樣的類別雖然可以保持簡潔,我們又要面對在一個類別中會同時面向兩個角色的問題。

往更小的分類方向去看,我們會發現一直會改變的是 calcCreditCardFee() 這個方法。如果我們把 calcCreditCardFee() 抽出來獨立使用呢?

interface FeeCalculator {
public function calc();
}
class CreditCardFeeHandler implements FeeCalculator {
public function calc(percentage) {
return ....;
}
}
class TaxFeeHandler implements FeeCalculator {
public function calc(percentage) {
return ....;
}
}

我們把 calcCreditCardFee() 變為 CreditCardFeeHandler 類別,再在裡面創建一個 calc() 方法。這樣我們不管是那位使用者要使用計算信用卡手續費,只要透過 CreditCardFeeHandler 就可以實例化它的計算方法出來。

另外,可以看到我們使用了 interface 並分別實作到 CreditCardFeeHandler 與 TaxFeeHandler 上。這樣我們的計算類別都可以有效解耦,而且在要注入的情況時,又可以獲得多型的好處。

(不了解 interface?看看物件導向中的介面與抽象類別是什麼 ?)

最後一個疑問,這樣整個結構不就變成一個 function 一個 class 嗎?
是有這樣的可能的!但是我們要了解到,你要實作一個良好運作的模組,除了對外的 function 時,我們其實還有很多資料與方法是私有的。只要這些內容都指向同一個使用者,很多 class 的 SRP 是可以被接受的。

總結一下,角色就是一群會使用該模組的使用者,可能是真的人,也可能是別的模組。所以我們在考慮模組對應的角色時,可以從模組會被誰使用出發。

而這樣做的好處就是可以「分開不同角色所依賴的程式碼」,從而減少不同模組因過度耦合而在改變時所造成的錯誤,同時亦可以更容易的進行測試。

老實說 SRP 可能 SOLID 原則中最不好理解的,如果大家對我所理解的 SRP 有不一樣的意見,歡迎下面留言一起討論!

如果你覺得我的文章幫助到你,希望你也可以為文章拍手,分別 Follow 我的個人頁與程式愛好者出版,按讚我們的粉絲頁喔,支持我們推出更多更好的內容創作!

--

--

YC
程式愛好者

提供更精確的技術內容為目標,另創立「程式愛好者」專頁。資深軟體工程師,專研後端技術、物件導向、軟體架構。