菜雞與物件導向 (10): 單一職責原則
我們在前面的 內聚和耦合 有提到過,內聚並不是無腦把相關的程式碼都封在一起就好了,也有分成健康的和不健康的。但我們要怎麼知道這個類別是否足夠健康呢?單一職責原則就是很好的檢驗方式,這篇就讓我們來紀錄一下。
單一職責原則 (Single Responsibility Principle)
「單一職責」原則顧名思義,就是一個類別應該只負責一個職責。
但是這樣太過籠統了,「職責」相當容易產生誤會,容易變成各說各話。畢竟咱們工程師最愛戰定義了嘛。
「你這類別不優,它有兩個職責!登入跟登出!」
『沒有啦,我這個類別就是負責帳戶管理的啊』
「OSSO。乾脆你全部放一起,然後說是負責網站管理算了,呵」
『……你存心來找碴的是不是?』
為了避免像這樣產生職場糾紛,我們需要先定義一下什麼是「職責」。經過前輩們的努力(解釋)之後,單一職責的定義就成了:
就一個類別而言,應該只有一個引起它變化的原因
另外,我也看過「一個類別應該只對一個角色負責」的說法,這兩者的核心概念是一樣的。
這邊讓我們簡單舉個例子。如果在訂單管理的類別中有一個新增訂單的方法,在收到訂單之後,會依序處理訂單、並取出會員的聯絡資訊,再依靠聯絡資訊寄送通知信件給會員。但它的實作全靠自己來,如下:
新增訂單()
{
// 收到訂單
/*
一些訂單的商業邏輯
*/
// 寫入訂單
/*
一些和資料庫連線寫入資料的處理
*/
// 取得聯絡資訊
/*
一些連到資料表或服務拿會員資料的處理
*/
// 寄送通知
/*
一些寄送信件的處理,如寄送者和寄送方式等等
*/
}
這樣一路就是流水帳打完收工,這樣的一個函式參雜了一堆不相干的邏輯,可能動輒數百行,每一段都處理各種不同的工作,一看就很明顯違反單一職責原則。
當訂單處理的商業邏輯、查詢會員資料的邏輯或是通知會員的方式有變更的時候,這個函式都會受到影響,也就是說這個函式同時對多個不同對象負責。這樣的類別或函式就是不穩定的。
遇到這種情況,我們可以將其拆分。讓上帝的歸上帝,讓凱薩的歸凱薩。
例如說會員的處理一律封裝回會員管理類別,我們再藉由會員管理類別去調用其方法取回資料;寄送信件也封裝到通知管理類別,不用去管用什麼方法通知的,我們只需要去要求其通知即可。
有些朋友可能會有疑問,這樣不就會和會員處理類別、通知管理類別之類的其他類別有了耦合關係嗎?有這樣的疑問是很合理的,這也就是為什麼我們會需要介面來讓類別之間不要直接彼此依賴,這部份我們在 介面 有詳細介紹。
新增訂單(訂單)
{
處理訂單商業邏輯(訂單)
訂單資料存取服務.儲存訂單() // 可能由資料存取層或連線管理等該職責的地方去實現
通知服務.寄送訂單通知(訂單.訂購人編號)
}
處理訂單商業邏輯(訂單)
{
// 專注在處理商業邏輯,不用管其他事
}
// 其他的職責拆分出去給負責該工作的類別
通知服務 { 寄送訂單通知(編號); }
訂單資料存取服務 { 儲存訂單(); }
我們把工作交給負責該職責的類別去做,自己只需要關注在自己正在處理的職責即可。聰明的朋友可能已經注意到了,這就是封裝的體現。封裝得夠舒服,我們就能舒服地處理自己的事情就好,這就是分工合作的偉大呀。
(2022-01-23) 補充:
前面提到的「職責」的部分,雖然我們前面提過了是「引起變化的原因」,但可能還是太過模糊,
畢竟咱們工程師真的最愛戰定義了嘛。今天在群組裡有前輩分享了講解單一職責原則的影片:Fred 聊聊 SOLID 設計原則,其中單一職責的部份,將前述的「引起變化」從業務需求變更的方式切入
影片中用實例來說明什麼時候該切分職責和其重點,例如 「業務耦合造成的問題就是職責不明確」、「不要讓類別去碰它不該做的事情等等」 ,我個人覺得非常不錯,推薦給想更了解單一職責原則或 SOLID 的朋友
雖然文章中的例子相對簡單。但有一個部份我個人覺得要特別注意:單一職責當然也可以用在函式上。甚至資料表或任何需要管理、分類抽象事物的東西上。
有些朋友可能跟我前陣子一樣,覺得函式就是用來消除重複的程式碼,直到我看了 可不可以不要寫糙 code 和一句「難道只有重複才需要做成 Function 嗎?」才明白:函式真正的工作其實是封裝邏輯。
既然是封裝邏輯這種抽象的東西,必然也會有其職責,自然也得好好注意單一職責囉。
走向單一職責
我們可以從上面的差別重新思考,遵守與不遵守單一職責原則會有哪些顯著的差異。
當我們並未遵守單一職責原則時,同個類別裡面充斥著不同工作的處理邏輯。也就是不健康的內聚:完全不夠聚,就只是盤散沙。
-
容易產生意外的重複。每個類別每個方法都自己去查詢會員資料,當查詢會員資料的方式或規則有變更的時候,影響範圍就會非常大,同樣的事情有一大堆地方要改,還得要先全部找出來,想到就頭痛。
-
同時我們在修改時也無法界定邊界,無法確定這次修改影響到的範圍,我們並不知道這些放在一起的東西,或是同一段做的所有事之間是否會相互影響,這將導致每次修改的時候都在挑戰我們自己的心臟負荷量,讓維護變成試膽大會,類別變成危樓改建。
-
承上,我們為了要確保修改沒有問題,我們必須大量閱讀不相關的程式碼,無形中造成開發負擔,降低開發效率。
如果你曾經有閱讀別人的程式碼,卻始終看不懂這東西到底在幹嘛,每分鐘髒話數筆直上升的經驗。答應我,我們不要讓別人經歷相同的悲劇,我們要斬斷仇恨的鎖鏈。我們,今天就開始走向單一職責。
當我們終於選擇單一職責,我們的類別才能真的擁有健康的高內聚。以上的這些問題,也都變成:
-
每個類別都專注在自己職責上,需要這個功能的其他類別就能來使用。大大提高了程式碼的重複使用程度,同時也降低了程式碼的重複性。並且因為類別內都是朝同樣職責前進的成員,彼此關聯性相當高,因此也提高了內聚。這兩點讓我們能迴避掉「要改的地方太多了,就改天吧」的悲傷結局。
-
同時,當我們要修改時,只需要找到負責的類別修改。因為已經把不屬於職責的工作交給其他類別了,達到了封裝和隔離,所以我們就能輕鬆看出修改的區域和邏輯,並較少地被不相干的東西影響、馬上掌握修改的目標和影響範圍,使得架構和類別更容易管理。也就是說,單一職責可以達到降低耦合的效果。
展現你的意圖
單一職責讓我想到前陣子看的《先問為什麼》中的芹菜測試:當你在超市結帳時,手上拿著巧克力、豆漿、餅乾跟芹菜,沒有人看得出來你到底要幹嘛。
寫程式也是如此,如果你的類別或方法裡什麼都要,彼此間又甚無關連,那就沒人看得懂這到底是幹嘛的。
如果團隊的其他人不能瞭解這個類別的職責,那後續協助修改的時候就會沒辦法把相同工作的程式碼歸類在一起,甚至難以修改,做起事綁手綁腳,新增個方法都會陷入混亂。整個架構就會開始腐敗。這也就是為什麼我們需要保持程式碼的可讀性,並且盡力實踐單一職責。
如同我在先問為什麼文中所引用的「你的一言一行,都要能證明你的信念」。在這裡,你的類別、方法,甚至是程式碼中的每一區塊,都必須要能夠展現你的意圖。
因此,單一職責不只能用來檢驗類別。從一整個服務,到單一個函式,都可以用它的意圖來問問自己。這一段是否只有一個職責?是否只有一個原因造成改變?職責是否清晰?
當然,從模組到函式每一層級的抽象概念是不一樣的,模組有模組關注的點,函式有函式關注的點,其規模有所差異,請不要用函式的職責大小去要求整個類別,我個人覺得這中間的差異還是挺吃經驗的,但不去嘗試思考,就沒得經驗可說嘛。這邊還是鼓勵大家多多利用單一職責去檢驗任何片段的程式碼。
當我們利用單一職責原則去檢驗,或是思考方向的時候,如果列得出兩項以上的變更原因,且這些原因彼此關聯很薄弱的時候,就是警訊。
反過來說,即使有兩個原因引起變化,但這些原因之間的關聯很強,例如總是一起變化,那其實就不必分離,或是可以暫緩分離,避免過度設計所引起的不必要的複雜性。(白話文來說就是走火入魔)
如果能做到撰寫功能當下,或是重構的時候不斷自我檢驗,那寫出來的程式碼品質相信也能展現出一定的水準了吧!共勉之。
本文整理時主要參考了這兩篇,寫得相當不錯,想更瞭解的朋友可以參考一下:
看到這篇覺得很不錯,從另一個角度切入單一職責,回來補充給各位朋友:
本系列下一篇:菜雞與物件導向 (11): 開放封閉原則
參考資料
- 再談物件導向設計原則: 單一職責原則,定義、解析與實踐 - WadeHuang的學習迷航記
- SOLID 之 單一職責原則(Single responsibility principle)- Miles - iT邦幫忙
- 深入淺出單一職責原則 Single Responsibility Principle - jyt0532’s Blog
- Fred 聊聊 SOLID 設計原則
- Object Oriented 物件導向設計原則 SOLID-1:Single Responsibility Principle(SRP) 單一職責 - Sian
- SOLID-I 單一職責原則(Single Responsibility Principle) - 黃子源 - Medium
- 《大話設計模式》 Ch3. 拍攝UFO --單一職責原則
- 《無瑕的程式碼:整潔的軟體設計與架構篇》 Ch.7 SRP --單一職責原則
- 《無瑕的程式碼:物件導向原則、設計模式與C#實踐》 Ch.8 SRP --單一職責原則
同系列文章
- 菜雞與物件導向 (0): 前言
- 菜雞與物件導向 (1): 類別、物件
- 菜雞與物件導向 (2): 建構式、多載
- 菜雞與物件導向 (3): 封裝
- 菜雞與物件導向 (4): 繼承
- 菜雞與物件導向 (5): 多型
- 菜雞與物件導向 (6): 抽象、覆寫
- 菜雞與物件導向 (7): 介面
- 菜雞與物件導向 (8): 內聚、耦合
- 菜雞與物件導向 (9): SOLID
- 菜雞與物件導向 (10): 單一職責原則
- 菜雞與物件導向 (11): 開放封閉原則
- 菜雞與物件導向 (12): 里氏替換原則
- 菜雞與物件導向 (13): 介面隔離原則
- 菜雞與物件導向 (14): 依賴反轉原則
- 菜雞與物件導向 (15): 最少知識原則
- 菜雞與物件導向 (Ex1): 小結
哈囉,如果你也有 LikeCoin,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)