SOLID-SRP 單一職責原則

做好一件事情就好

Timothy Liao
Coding Book Club
8 min readAug 7, 2022

--

前言

在Docker之後,讀書會下一期的目標是SOLID,希望在開發時能夠藉著這些原則避開一些前人已經踩過的坑,並且讓程式兼具維護性及擴充性。

在剛轉職之初,就曾聽過Will保哥分享關於SOLID的部分,當時的內容對我來說無異於天書XD停停頓頓的,最終還是沒有把課程聽完;而到了開始在實務場域工作後的現在,為了讀書會將內容重新聽來,才總算對於其中的細節有了一些同感。

其中提到程式設計原則帶來的幫助,對我來說印象頗深刻,就以此作為開場吧。

「原則帶來的幫助在於行為及價值評量,行為指的是你能基於此些原則寫出比較好的程式碼,而價值評量指的是能夠判斷程式碼的好壞。」 — Will 保哥(內容不精確印象XD)

好的,接著來看SRP-單一職責原則(single responsibility principle)吧

在SOLID之前的前情提要

因為在此章甚至後續的原則分享中,撰寫者都會提到一些技術字彙,為了確保我們都在同一個概念上,我們先來定義以下幾個字彙。

模組(module)

具有相同概念、範圍的一組程式碼。

就我個人的角度而言,他是一個抽象概念,並未特定指稱某個明確的範圍,所以小至function/method,大至一個class,或甚至是MVP內的M, V, P,你都可以稱作是模組。

內聚力(cohesion)

模組完成(關注)單一工作(任務)的指標(度量標準)。

從開發的角度上來說我們會盡可能達成高內聚的程式碼,讓每個模組的職責明確。

function sum(...numbers) {
let final = 0;
for (const num of numbers) {
final = final + num;
}
return final;
}

以上面的加法function而言,應該就可以說這個模組具有高內聚力。

耦合力(coupling)

模組間相互關聯(影響)的強度。

盡可能達成低耦合,目的在於修改模組時能有越少的其餘模組受到修改的影響。

這邊我嘗試著以上述的程式碼舉一個例子,如果有一個遊戲營運商,希望遊戲開發部工程師偷偷修改加法,以後加法加超過10000後增加的數字要偷偷除以10,結果會計部門的工程師,並不知道這個method已經被偷改過了,就拿來計算薪資、營收等等的…

這就是高耦合的狀況可能會發生的,改A壞B。

內聚 vs 耦合

關於這兩種模組間的力,最希望達到得就是高內聚、低耦合。

但從前面關於耦合力的例子,可以發現,這兩者之間其實是有互斥的部分存在。

比如:如果會計部門使用自己的sum method,那他就不會受到遊戲部修改method的影響,我會說這樣耦合降低,但內聚力也降低了。

或是,如果有一個code有數萬行,他可能是超低耦合,但可能同時也是超低內聚吧XD

所以也不是單純追逐高內聚、低耦合,而是要找到一個平衡,希望經過一輪SOLID讀書會,能對於此有基本認識,那接著就來進入SRP吧。

耦合力

“A class should have one, and only one, reason to change.’”

原文是這樣,我個人的白話翻譯是:一個模組只應該因為一個理由被改變(修改)。

就我的理解及找到的資料來看,關於「理由」,有兩種角度解釋:

改變的原因

譬如說,如果今天有一個使用者的付款功能,該功能包含付款及紀錄付款狀況的資料。

那這個付款的模組就同時有了「兩個理由」在變更時需要修改這個模組,付款相關以及紀錄相關。

角色

通常功能都是層層漸進疊上的,因此也導致了模組的污染。

以下圖為例:建立訂單得的組件,內部可能包含驗證資料、連線db、通知用戶訂單已建立。

初始的模組

當需求迭代時:

  1. 使用者可建立訂單
  2. 使用者使用現金付款
  3. 付款後要加上app推播
  4. 再加上email通知
  5. 使用者使用applePay付款

模組可能會變得如下這樣:

集中單一模組

按照服務的角色來區分的話,其實可以分為訂單、通知、付款

所有的組件都集中單一模組導致的缺點

最後思考了關於違反SRP可能會造成的問題

  1. 類別複雜度過高
  2. 維護時找不到要改哪裡
  3. 發生邏輯問題時找不到bug在哪
  4. 使用類別時不知道要呼叫哪個方法
  5. 較難進行測試(以function的角度思考)

使用時機

那什麼時候我們能嗅到使用SRP的時機呢?

一、兩個責任在不同時間點產生變更需求

以下圖為例:

當我們發現這個建立訂單的method包含多個修改的需求時,例如:換db,通知的方式…等,就代表有可以使用SRP的機會了。

例如通知的方式除了推播又新增e-mail,這時候或許就是重構模組的好時機了。

二、組件重用的時機

例如:若某些模組中的程式碼在其他地方也有使用需求時,也可考慮基於SRP將它在從該模組中獨立出來。

三、除了類別或方法設計外,也可以作為思考資料庫欄位設計的原則

最明顯的做法是,一個欄位不該同時有兩個意義。

例如:訂單的PK當時在設計時盡量避免同時作為分類的依據。

益處

在前面說了這麼多,其實SRP帶來的好處也多次提及了,不過我個人要明確定義好處的話,SRP的好處在於:

「保持程式碼意圖」。

透過隔離意圖不同的程式碼,例如訂單、支付…等,避免修改時看到太多與當前需求不符的code。

益處

前面曾提到關於內聚與耦合之間的互斥,SRP其實就是體現提高內聚力的原則,但他同時也造成了耦合力的升高。

就我的角度來看,其實從JS的模組引用或是各種語言原生的方法都是SRP的產物,同時間他的耦合力也跟著提高了。

比如之前相當有名的faker套件,他讓我們得以用套件完善的隔離了所有假資料的相關方法,但是當套件本身出現問題時,所有依賴於該套件的程式也都因此出現了問題。

另外如果過度設計導致每個方法都使用一個類別,其實也不會讓維護性變高,反而可能更難用了XD

副作用

SRP也常是大家困擾的點,他讓人最大的困惑就來自於,到底那個「reason to change」的reason究竟指的是什麼呢?

同一個模組,每個人在思考該模組的reason不同,就可能導致截然不同的設計。

因此有些平衡的原則是我覺得很適合作為SRP的補充的。

YAGNI

是「You aren’t gonna need it」的縮寫,此原則包含以下幾點:

  1. 不急於第一時間分離責任
  2. 對於未出現的需求不需要預先分離
  3. 需求變更時再進行模組分割就好

除非對於業務需求相當了解,並具有相當領域知識的開發經驗,大部分時候我認為我們並無法在開發初期就能妥善拿捏應該使用SRP的範圍。

比如前面曾提到,多個資料庫的應用可能就會是需要使用SRP的部分,但以我所看到的大部分的應用程式的生命週期,使用單一資料庫的狀況可能是一種常態,那是否需要在程式開發的一開始就為此做模組的拆分是值得討論的。

Over-Enginerring

這可能無法稱得上是一種原則,但我覺得他會是對於SRP這個原則的平衡。

每個工程師都應該有開發的重心及優先順序,且每個工程師的時間都應被視為成本,過度追求SRP以及分離職責會使人忽略應進行的優先順序、及成本的考量,可能導致最小可行性的程式無法完成。

與YAGNI類似的角度是,我們選擇當業務邏輯擴增時才進行拆分,並將此當成是一個迭代的過程。

額外的思考原則

在看完找到的相關資料後,我個人有個結論是:

「原則的實行,仍必須建立在對於需求的了解之上」

特別是SRP這樣有著模糊定義的原則,如何定義每個模組的改變原因,就很需要此結論作為背書。

至於因為內聚力的提升導致的耦合該怎麼處理呢?

接著我們就會開始介紹OCP-開放封閉原則。

結論

再談物件導向設計原則: 單一職責原則,定義、解析與實踐

使人瘋狂的SOLID 原則:單一職責原則(Single Responsibility Principle) | by YC | 程式愛好者| Medium

Fred聊聊SOLID設計原則

菜雞與物件導向(10): 單一職責原則

http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

深入淺出單一職責原則Single Responsibility Principle

Uncle Bob - The Single Responsibility Principle

參考資料

--

--