SOLID-SRP 單一職責原則
做好一件事情就好
前言
在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、通知用戶訂單已建立。
當需求迭代時:
- 使用者可建立訂單
- 使用者使用現金付款
- 付款後要加上app推播
- 再加上email通知
- 使用者使用applePay付款
- …
模組可能會變得如下這樣:
按照服務的角色來區分的話,其實可以分為訂單、通知、付款
所有的組件都集中單一模組導致的缺點
最後思考了關於違反SRP可能會造成的問題
- 類別複雜度過高
- 維護時找不到要改哪裡
- 發生邏輯問題時找不到bug在哪
- 使用類別時不知道要呼叫哪個方法
- 較難進行測試(以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」的縮寫,此原則包含以下幾點:
- 不急於第一時間分離責任
- 對於未出現的需求不需要預先分離
- 需求變更時再進行模組分割就好
除非對於業務需求相當了解,並具有相當領域知識的開發經驗,大部分時候我認為我們並無法在開發初期就能妥善拿捏應該使用SRP的範圍。
比如前面曾提到,多個資料庫的應用可能就會是需要使用SRP的部分,但以我所看到的大部分的應用程式的生命週期,使用單一資料庫的狀況可能是一種常態,那是否需要在程式開發的一開始就為此做模組的拆分是值得討論的。
Over-Enginerring
這可能無法稱得上是一種原則,但我覺得他會是對於SRP這個原則的平衡。
每個工程師都應該有開發的重心及優先順序,且每個工程師的時間都應被視為成本,過度追求SRP以及分離職責會使人忽略應進行的優先順序、及成本的考量,可能導致最小可行性的程式無法完成。
與YAGNI類似的角度是,我們選擇當業務邏輯擴增時才進行拆分,並將此當成是一個迭代的過程。
額外的思考原則
在看完找到的相關資料後,我個人有個結論是:
「原則的實行,仍必須建立在對於需求的了解之上」
特別是SRP這樣有著模糊定義的原則,如何定義每個模組的改變原因,就很需要此結論作為背書。
至於因為內聚力的提升導致的耦合該怎麼處理呢?
接著我們就會開始介紹OCP-開放封閉原則。
結論
使人瘋狂的SOLID 原則:單一職責原則(Single Responsibility Principle) | by YC | 程式愛好者| Medium
http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
深入淺出單一職責原則Single Responsibility Principle
Uncle Bob - The Single Responsibility Principle
參考資料