什麼是好的程式碼?從 SOLID 的設計原則出發(一)前言 & 單一職責原則(Single Responsibility Principle, SRP)

ChengYang
8 min readJun 14, 2022

--

基隆海邊

前言

身為一位軟體工程師,幾乎每天都在與程式打交道,我們一定會常常想著自己寫的 code 不要日後給自己或他人造成困擾,所以一定要提升寫 code 的品質,那麼在優化自己 code 的過程,八成會研讀 Clean Code (無瑕的程式碼:敏捷軟體開發技巧守則) 或者 物件導向的 SOLID 設計原則 ,那今天我們就簡單聊聊 SOLID 設計原則。

什麼是 SOLID ?

維基百科來看

程式設計領域, SOLID單一功能、開閉原則、里氏替換、介面隔離以及依賴反轉)是由羅伯特·C·馬丁在21世紀早期[1] 引入的記憶術首字母縮略字[2][3],指代了物件導向程式設計物件導向設計的五個基本原則。

  • 單一職責原則(Single responsibility principle, SRP)
  • 開放閉合原則 (The Open/Closed Principle, OCP)
  • 里氏替換原則(Liskov Substitution principle, LSP)
  • 介面隔離原則(Interface-segregation principles, ISP)
  • 依賴反轉原則(Dependency inversion principle, DIP)

那我們都知道我們希望軟體可以「迅速的可用版本迭代」以及「訊息透明流通」來降低軟體專案的交付風險。所以需要實現敏捷開發,那麼系統與團隊要達到敏捷開發的要求,也需要透過一些系統設計原則來避免影響系統的可維護性的設計壞味道,使系統變更更有效率且更低的成本。

Design smells 設計壞味道

上述提到了我們應該避免設計壞味道,那什麼是設計壞味道?從維基百科來看

計算機編程領域,設計所用結構違背了基本設計原則並對設計質量有負面影響[1]這個術語源自馬丁·福勒在《重構-改善既有代碼的設計》一書中描述的代碼異味[2]

所以簡單來說就是一個不好的設計,對未來軟體的開發維護會造成困擾。

這邊舉幾個比較經典的例子

  • 僵化(Rigidity):難以變更,系統難以修改,因為每個改變迫使系統其他部分也要做出修改。
    舉例來說:像是你調整了 A 功能,結果 B, C 功能都需要跟著變動,這代表他們之間關係可能太過耦合,互相依賴,也會使維護者超出評估。
  • 脆弱(Fragility):容易損壞,一個修改可能引發沒有概念關係的其他地方壞掉。
    舉例來說:像是一個新增了 A 功能,結果跟它毫無相干的 B 功能會壞掉。
  • 頑固(Immobility):難以復用,很難把系統分離為可被其他系統重用的組件。
    舉例來說:我們有個 A 類別 ,它跟其他類別的細節太過依賴,假設發現要把它們之間重複的地方獨立出來的成本高於直接自己重寫一個的成本,那就是 Immobility
  • 粘滯(Viscosity):很難正確的修改,做正確的事比做錯事更難。
    舉例來說:想像我們現在有一個 function ,可以在畫面上顯示訊息,在這 function 裡面我可能一開始要先找到當前要顯示的 view ,然後開始製作訊息的 view ,製作過程要設定位置、大小、文字內容和外觀風格…之類,最後把這個訊息 view 顯示在當前的 view 上,這是一個一連串流程,那假設我們今天要調整這一串流程的其中一段或者新增一段邏輯,但發現很難調整,可能要在某個地方加 flagif else 判斷有的沒有的東西,那這很有可能就是 Viscosity 。

看完以上這幾點能發現,大多數問題可能發生於組件之間有過強的相互依賴性( interdependence)

所以為了我們的系統持續開發中,不要出現設計壞味道,我們需要持續透過一些好的原則和模式來主導我們開發及維護,我們的終極目標就是使系統隨時都盡可能 「簡單」 、「整潔」 、「具可維護性」 ,也是 SOLID 的本質,試圖解決組件之間過強的相互依賴性

到目前為止,相信我們都知道為什麼需要 SOLID 且也知道它想幫我們解決什麼問題了,接下來就進入我們的主題。

單一職責原則(Single responsibility principle, SRP)

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

單一職責原則是 SOLID 中,描述與定義最模糊的一條。

  • 一個類別中應該只有一個被改變的原因(reason to change)
  • 一個類別應該只做一件事,即其職責
  • 其職責就是該類別會被改變的原因

看完這三點敘述可能會覺得有點鬼打牆,簡單來說我們都知道單一職責就是做好它「應該」做自己事情,用一個現實簡單的例子說,你有一台相機,按下快門就是拍照,而不是跳出記憶卡,也不會是調整光圈或快門,在按下快門時它該做的事情就是拍照。

回到寫軟體想像一下假設今天有一個需求是用戶管理功能,要有新增/刪除/修改/查詢,那我們是要建出四個類別還是一個?可能有些人會覺得需要四個,因為每個都是獨立的職責,但真的是這樣嗎?所以我們要先釐清什麼是「職責」?

看一個例子,假設今天我們有一個需求,需要「創建用戶」,然後我們建立了 User 類別,裡面有用戶 ID 和等級(UserRating)

接下來又收到一個新的需求,我們要讓「用戶綁定信用卡」,但是需要依據用戶等級來決定是否可綁卡,如下:

整理一下,目前有兩個需求,分別是「創建用戶」以及「用戶綁定信用卡」,這兩個需求都和 UserRating 建立耦合了。

再來又來了一個需求,「調整用戶等級」,我們原本只有分三個等級,現在變成五個,所以 userRatingbindCard() 都需要跟著變動了,如下:

最後收到新需求,「調整信用卡綁定功能」,這時我們都知道要去調整 cardIDsbindCard() ,但是關鍵的 userRatingbindCard() 真的能安心使用嗎?你可能覺得當然可以,目前都還很單純,但假設未來需求越來越多,功能更複雜且就像上面的例子一樣一直互相依賴時,真的還可以安心的使用嗎?

組件互相依賴所以造成系統可維護性問題,通常會越來越嚴重,然後形成很難更動的技術債,我們都知道我們的軟體會隨著業務需求變動而改變,依這例子的可維護性問題,其實最根本是「單一組件中業務耦合的問題」,這個可能比組件之間的耦合還更麻煩,我們一個 User 類別裡,需要處理用戶需求也還要處理信用卡需求,所以要遵守 SRP 原則的話,它們兩一定必須分開來,然後共同要有的東西,可以抽象化(Swift protocol 或 Java interface)。

所以 SRP 強調的是:一個組件會發生變更的原因(即職責),應該來自同一個業務關聯方、同一群會對組件提出業務需求的人;亦即決定組建之「職責」為何的是「人」,而非其他組件或系統。

遵從 SRP 後,單一組件需要變更的業務原因將單純化,因業務耦合造成的維護的問題將大大減少。一個組件不該它做的事情就不能與其有關聯。

SRP 的概念除了可以用在「類別設計」、「方法設計」外,「資料表」和「資料欄位」的設計,甚至更大規模的「軟體組件」或「微服務」的設計。

那最後想判斷我們到底有沒有遵守 SRP 呢?只要問:「這個 XXX(組件/服務/類別/表/欄位/方法) 是做什麼用的?」如果得到的結果是包含了兩件事以上,那就很有可能違背了 SRP 。

--

--