閒談軟體設計:Single Responsibility

更新於 2024/10/26閱讀時間約 10 分鐘
Single Responsibility Principle (LearnStuff.io)

Single Responsibility Principle (LearnStuff.io)

話說在前頭,這篇文章所提出的質疑不一定適用所有的情境,請依據讀者自己的情境酌量採用。

理所當然?

這次聊聊一個幾乎每個軟體工程師都會掛在嘴邊,但實際上寫程式是否還記得的原則:Single Responsibility。從 Wikipedia 第一行的說明

every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class, module or function.

很多人會覺得,這原則很簡單啊,但實際上卻有點模糊,什麼功能應該放在哪個類別 (模組) 或是不該放在那個類別 (模組) 的邊界卻很難拿捏。這邊說個就學時的故事,當年研究所課程助教幫我的作業做 design review,那時我把專案從檔案讀取和寫入檔案的 method 放在專案的類別中

當時覺得這樣的設計很合理,畢竟我不想替私有的成員加 setters,但助教建議我將讀取和寫入的邏輯移出,那時和助教有過熱烈的討論,最後被說服的理由便是 single responsibility,但助教的說法是 single responsibility 的另一種解讀:

A class should have only one reason to change.

如果要改變檔案的結構,或是改成讀寫 JSON 檔案,又或是要支援多種檔案格式,那要修改哪個類別?但這是Project這個類別的責任嗎?想想確實,這類別的責任應該是管理元件與之間的連結,其他都不是這個類別的責任,而且為了讀取 XML,Project類別和 JDOM 有了相依性。

生產力與單一責任原則的兩難

看到這,我想應該有人會認同助教的說法,於是我們來看下個例子,這應該很常見在 Java 處理 JSON 的轉換程式中看到:

而且我相信很多人這樣寫的時候,並不會想到 single responsibility 的原則,因為這樣寫很方便,提高生產力,同樣的類型,再看一個例子:

這寫法幾乎快變成 Java 後端的標準起手式了,畢竟回頭寫 XML mapping 的人應該越來越少了,當初第一次學 JPA 時覺得這個超方便的,而且程式和 mapping 放在同一個檔案,很容易發現 mapping 不一致的問題,但這樣寫真的符合 single responsibility 嗎?更何況不一致是雙向的,改 DB 欄位若沒想到回頭改 mapping 也是會不一致。

這情況不是 Java 獨有,自從 Swift 導入Codable介面後,實作Codable也幾乎變成標準寫法了 (若把 fullName 宣告成 computed property,可以不用寫init(from decoder)encode(to encoder),這是刻意寫的例子),不這樣寫還會被嫌,但若反問為什麼這樣寫比較好,到目前為止,我並沒有聽到任何決定性的優點讓我非這麼寫不可,我也沒那麼喜歡這樣寫。

友善的距離

至少,我會拆成兩個檔案:Customer.swiftCustomer+JSON.swift,可能有人會疑惑,程式碼明明沒有差 (其實有差,visibility 不一樣),為什麼要拆成兩個檔案,如果專案只有一個模組那確實沒太大差別,但如果有拆模組,例如CoreWebService模組,那Customer.swift會放在Core模組,另一個檔案Customer+JSON.swift會放在WebService模組,由於 extension 的 visibility 是 internal,所以在Core模組是看不到 JSON 相關的內容,任何和 JSON 相關的修改也不會動到Customer類別和Core模組,而是限縮在另一個模組裡,某種程度上還算是符合 single responsibility 原則。

因為官方提供了Codable,所以就一定要這樣寫?我個人是覺得沒必要,事實上我個人並不介意寫像這樣的程式碼跟相對應的測試:

回到單一責任原則

若真的要嚴格說,domain 類別的責任主要是封裝 domain knowledge,不在 domain 中的東西,都可以用 creator 或 pure fabrication 類別處理,而不用放在 domain 類別中。但有些東西就很難說,例如 create time 和 update time,通常在分析 problem domain 時,不會列出這兩個時間戳記,但幾乎在所有的商業應用都可以看到他們在 domain 類別中,但若仔細想,為什麼會需要這兩個時間戳記甚至是誰改的?主要是為了稽核這個目的,如果是這樣,這是各別 entity 的責任嗎?難道沒有其他的解法?

事實上是有的,雖然在 GSS 待不長,卻也跟當初的架構師學了一些技巧,那時用 Spring framework 的 AOP 寫一個攔截器,搭配自訂 annotation,攔截所有需要稽核的 Restful API end points,只要呼叫 API,攔截器便會被呼叫,此時可取得是誰呼叫 API 以及請求的內容,並在 API 結束後,攔截器再次被呼叫,取得回傳的內容,如此,就可以將所有 API 的呼叫記錄下來,甚至可以知道誰在幾點幾分下載了什麼檔案,或修改了什麼資料,如此一來在每個 entity 中加入 create time 及 update time 就不是必要的,雖然這樣做是最方便的 (但只能記住最後一次修改的時間)。

剛剛的 create time 及 update time,還能算作是 application logic,勉強能稱上是 domain 的一種,但因為使用的 framework 導入的東西,大多數都是實作層級的。除了剛剛的 annotation 外,因為導入 ORM 內建的樂觀鎖 (Optimistic Offline Lock),在物件中加入 version,也是一個當初我剛接觸時,不喜歡卻也離不開的東西,有了 version 資訊,ORM 工具就可以協助判斷這次存入的變動是根據哪個版本所做的,若 DB 其實已經有更新的版本時,ORM 會以例外的方式告知這個變動可能是過時的,這非常有用,畢竟後端要思考到多個 request 編輯同一份資料的情況,但總是覺得好像違反 single responsibility,若以 DDD 的角度,Aggregate 本身負責交易邊界,似乎還說得過去。

近期,不少程式語言 (Objective C、C#、Swift 和 Kotlin) 有 extension 的機制,在不修改原有類別 (像是其他函式庫的類別,無法修改其原始檔)的情況下擴充類別的功能,這機制十分強大,我也很常用,但這幾年,我開始覺得這機制也助長違反 single responsibility 的現象,因為方便,開始為某些類別加入 extension methods,而且越加越多。

我只能說,拜託在加入前請認真思考一下,這真的是這個類別的責任嗎?像是 derived information 用 extension 就很合適,例如先前例子中的 full name、或是用生日推算出年紀的函式,如果一開始沒有在類別中,用 extension 擴充就很不錯。但特定情境下才適用的 derived information,例如 UI 才適用的資訊,比起 extension 放在 view model 也許更合適。

另外,用 extension method 的可讀性真的比用 static utility method 高嗎?某些情況下真的比較高,像是在設計 DSL 時,可以寫出較接近句子的程式,但不是每個情況都是如此。

實際上 Kotlin extension method 轉成的 byte code 就是一個第一個參數是目標類別的 static method,然後提供一個語法糖衣。

但不同類別之間的轉換,用 extension 加到任何一邊都很怪,因為轉換這件事到底應該是誰的責任?雖然說,extension 可以把程式分到不同檔案,加上可以透過一些語言機制,讓某些 extension 的函式不被看見,但還是把功能加到原先的類別中,更何況不是所有的語言都有這類機制。

這也是我在讀過《Clean Architecture》後,重新思考的問題,不同 layer 之間資料的轉換,該是誰的責任?理想狀況是加入一個第三者:

但如果不加入第三者,我目前比較喜歡把責任放在接近 I/O 的類別身上:

總結

小結一下,若真的要符合 single responsibility,通常會得到很多很小的類別或是函式,各別完成一個小的功能,然後在某個地方被聚合起來完成一個使用案例 (use case),而不是一個很大的類別,包山包海,然後最後變成一個狀態超複雜,超級難測試的類別。也許是因為 dependency 難管理,所以很多工程師喜歡大類別,而不喜歡很多的小類別。

所以下次,當想把某個函式或屬性加到某個類別前,也許可以先想想 single responsibility,有沒有更合適的地方或是新增一個第三者類別。至於,那些為了提高生產力的寫法,到底在整個軟體開發週期中減少多少時間,我就不去討論了 (近一年都在寫 Node.js,沒有那些 annotations,也不會覺得有失去很多生產力)。


後記

現在想想,會對 JSON annotation 或是 serialization 有些堅持,真的跟當初與當時的助教 Teddy 熱烈討論後,留下深刻的印象有關。

這裡要小小抱怨一下《Kotline in Action》的中文翻譯,為什麼要把 extension 翻譯成繼承?我通常一本書不會一次看到完,有時會隔個幾週再回去看,然後看到繼承這個術語時,都還要根據上下文來判斷是類別之間的繼承還是 extension,我想超過 90% 以上學過 OOP 語言的人,看到繼承兩個字絕對不會聯想到 extension,翻譯成繼承真是爛透了。


延伸閱讀


avatar-img
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
這是幾年來我對於軟體架構師的心路歷程,上述不保證讓你成為軟體架構師,但希望會對軟體工程師職涯有幫助。也希望台灣的軟體公司能稍微多注重一下軟體架構,甚至能像 91App 不只工程師團隊,還有軟體架構團隊。
我個人是盡可能不寫 switch statement,但觀察這幾年程式語言的趨勢,會發現許多語言把 switch statement 擴充成為實作 pattern matching 的工具,說不定以後 switch statement 會越來越廣泛使用也說不定。
長遠的角度來看,內部函式庫還是值得投資的公司資產,只是它需要時間、人力與管理才能做得好。若有不錯的內部函式庫也可以回饋給open-source社群,畢竟,現在開發軟體已經不太可能沒有用到任何open-source的東西。雖然說是將公司資產以 open-source 釋出,但換取的利益卻不見得是零。
整結來說,受到幾種語言的影響,我個人設計 API 時,除了合乎該語言的 convention、上述的穩定性及一致性外,大致還會注意幾點:語意清楚、相近的顆粒度、簡單的文件、讓程式能像文章般閱讀。
第三方套件用了Promise或是Reactive,導致所有business logic都要做調整,這就違反「只能有對內的相依方向」的原則。business logic大多數情況下與效能優化無關,通常需要優化的是I/O的存取,這些既然都在外層,就應該在外層做優化,外層的優化不該影響核心,這才是好架構。
任何語言特性用與不用,其實要看是否提升了生產力?是否提升可讀性?是否提升可維護性?這些都是在三個月甚至半年後回來修改程式時,才能明顯感受到的,而不是寫程式的當下。Java 8 的 CompletableFuture、Stream 和 Optional 都很好,但用的不好反而畫蛇添足又沒提高可讀性。
這是幾年來我對於軟體架構師的心路歷程,上述不保證讓你成為軟體架構師,但希望會對軟體工程師職涯有幫助。也希望台灣的軟體公司能稍微多注重一下軟體架構,甚至能像 91App 不只工程師團隊,還有軟體架構團隊。
我個人是盡可能不寫 switch statement,但觀察這幾年程式語言的趨勢,會發現許多語言把 switch statement 擴充成為實作 pattern matching 的工具,說不定以後 switch statement 會越來越廣泛使用也說不定。
長遠的角度來看,內部函式庫還是值得投資的公司資產,只是它需要時間、人力與管理才能做得好。若有不錯的內部函式庫也可以回饋給open-source社群,畢竟,現在開發軟體已經不太可能沒有用到任何open-source的東西。雖然說是將公司資產以 open-source 釋出,但換取的利益卻不見得是零。
整結來說,受到幾種語言的影響,我個人設計 API 時,除了合乎該語言的 convention、上述的穩定性及一致性外,大致還會注意幾點:語意清楚、相近的顆粒度、簡單的文件、讓程式能像文章般閱讀。
第三方套件用了Promise或是Reactive,導致所有business logic都要做調整,這就違反「只能有對內的相依方向」的原則。business logic大多數情況下與效能優化無關,通常需要優化的是I/O的存取,這些既然都在外層,就應該在外層做優化,外層的優化不該影響核心,這才是好架構。
任何語言特性用與不用,其實要看是否提升了生產力?是否提升可讀性?是否提升可維護性?這些都是在三個月甚至半年後回來修改程式時,才能明顯感受到的,而不是寫程式的當下。Java 8 的 CompletableFuture、Stream 和 Optional 都很好,但用的不好反而畫蛇添足又沒提高可讀性。
你可能也想看
Google News 追蹤
Thumbnail
徵的就是你 🫵 超ㄅㄧㄤˋ 獎品搭配超瞎趴的四大主題,等你踹共啦!還有機會獲得經典的「偉士牌樂高」喔!馬上來參加本次的活動吧!
Thumbnail
阿揪西放送的老朋友們應該知道,今年我剛結束了一段大齡留學生活。這段時間偶爾有網友私訊詢問學校申請、開銷和準備流程等問題,我也樂於分享各種細節。其中常提到的建議之一就是:開通一個便捷的網銀帳戶。
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
徵的就是你 🫵 超ㄅㄧㄤˋ 獎品搭配超瞎趴的四大主題,等你踹共啦!還有機會獲得經典的「偉士牌樂高」喔!馬上來參加本次的活動吧!
Thumbnail
阿揪西放送的老朋友們應該知道,今年我剛結束了一段大齡留學生活。這段時間偶爾有網友私訊詢問學校申請、開銷和準備流程等問題,我也樂於分享各種細節。其中常提到的建議之一就是:開通一個便捷的網銀帳戶。
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作