Yuanchieh's Blog

Yuanchieh's Blog

生命是長期而持續的累積

15 Jun 2021

Youtube 直播「Fred聊聊SOLID設計原則」整理

當時在社群看到分享,想說就聽看看也沒什麼損失,聽完才發現根本賺到,沒有想過 SOLID 可以用這種方式理解,也才真正明白自己以往在看 Uncle Bob 的書思考都太淺層,真心感謝 Fred 大大撥空,而且在直播中遇到設備、網路問題還是很有條理的分享

以下將整理 Fred 大大直播的分享,還是建議有空可以花兩小時看完直播

SOLID

Uncle Bob 在 1995 年(約 37歲)根據自己開發程式的痛點,也就是大型軟體程式可維護性,需求變更帶來程式碼維護問題,而維護問題的根本原因是程式碼的耦合,在社群提出討論,然後被戰翻了 XD

設計壞味道

  • Rigidity 僵化:任何變更會導致系統中相依的組件需要變更,超出想像
  • Fragility 脆弱:發生變更時,其他地方容易發生問題
  • Immobility 難以服用:組件有太多細節的依賴
  • Viscosity 黏滯:變更時用太多 hack,而非以原設計的方式進行

組件相互依賴性

SOLID 本質在解決組件之間不合理的相互依賴性

Single Responsibility

描述與定義最模糊的一條,有幾種常見說法

  • 一個類別應該只有一個改變的原因
  • 一個類別只應做一件事,就是他的職責 => 那職責應該是什麼,就是這個類別該做的事 ?! 例如一個學生管理系統,Student 的類別增刪改查,要產生幾個類別? 資料處理中的 ETL 是一個職責還是三個原則?

違反 SRP 的設計可能會長這樣

可維護性問題方面,除了程式碼耦合,也要注意 業務耦合 一個組件有了用戶相關,又有信用卡相關,結果兩種業務就耦合了

軟體組件發生的原因(職責),應該來自同一個業務方,也就是同一群會對組件提出業務需求的人

當組件規模增加,業務量增加,考慮使用 SRP 拆分組件,讓業務單純化,持續迭代,從另一個角度理解 SRP 是組件不該碰的事情別碰

識別是否遵從 SRP 的提問「這個 xxx 是做什麼的?」/ 如果答案包含了「ooo xxx」,則通常違背了 SRP

常見錯誤

  1. 不要用訂單 ID 格式當作分類的方法,而應該用獨立的欄位 (ex. 突然要改 id 長度就爆炸了)

透過以下問題思考 SRP 方針

  1. 不一定要拆,如果 20 個方法是緊耦合也屬於同業務
  2. 可以,但計算方式應該拆除
  3. 分開
  4. 最好不要,用戶等級就是等級,狀態就是狀態

小結

  • SRP 建議:任何一個組件應該只對同一個客戶/業務關聯方的需求而發生變動
  • 不該做到的事就不要碰
  • 識別是否違反 SRP : 這個組件/服務/類別/欄位/值是做什麼的
  • 從被更動的組件角度解決業務的耦合

Open Close

如果每次功能修改都會造成系統一連串的組件修改,這樣就不符合開閉原則,一般會理解成不修改原始碼就可以擴展系統行為 ?!這是通靈嗎 XD 怎麼可能沒有寫 code 行為就出現

真正的解讀是

系統要可以妥善預測複雜度的發生點,並建立適合的 擴展點 ,而發生行為變更時,透過擴展點變更,原本的主流成與使用該擴展點本身的 client 本身不需要修改

例如有一個外賣平台,有多種會員身份,對應不同的折數,

calPrice(){
	if(用戶是專屬會員) {
		if(金額 > 200){
			打七折
		}
	}

	if(用戶是 vip 會員){
		打八折
	}

	if(普通會員){
		if(上個月還是 vip 會員){
			打八折	
		}
	}

	原價
}

如果 PM 説

  1. 會員制度增加
  2. 調整折數 則整個計價都會受到影響

所以複雜度發生點在於 計算應付價格 ,應該在此處設計擴展點,透過 Strategy Pattern,建立 UserPayService interface,將計算邏輯放在不同的實現上

如果發生

  1. 增加會員等級 ⇒ 增加實作即可 原本計算的流程不需要修改

可以參考 用多型取代重複的判斷式

小結

組件的耦合,會讓微小的變動也讓系統有大幅度修改,OCP 建議:組件的設計必須讓系統易於擴展,同時限制每次被修改的範圍,實作為將系統劃分一系列的組件,將依賴性關係依照層次結構組織 (穩定的組件不要相依於不穩定的組件),套用到架構設計也是如此 (Clean Architecture)

OCP 也是一個很好結合 Design Pattern 的原則

依賴反轉原則

新提出的設計架構,會分層不同的業務

例如網頁操作會從 web ui 層 => controller 業務邏輯 => 儲存到 db 層再原路返回給 client,寫程式時如果按照這樣的順序,有可能發生高階組件依賴低階組件,例如客戶匯出雲端發票會儲存於 S3,那我們很容易把 s3 上傳邏輯寫死雲端發票的業務邏輯中,但如果 s3 sdk 升級怎麼辦?

仔細想想「發票匯出」的業務需求跟「實際使用哪一間雲服務」有關係嗎? 所以需要一個 彈性組織組件的依賴 ,將能力抽象化出業務規則,明確定義出介面方法

小結

  1. 為了達成 OCP 效果,需要做組件的切片的方式 與 彈性調整組件依賴關係的方法
  2. 程式碼依賴關係應多使用介面,而非具體實現

里氏替換原則

70 年代軟體越來越大,開發的成本越來越高
⇒ 加入流程控制 / blocks / subroutines 分解多個組件 (modules),組合出結構化的系統
⇒ 經常有多個功能類似的組件組件,讓這些組件組件之間有關聯的狀態,smalltalk 加入了 繼承
⇒ Liskov 於 1987 年發現繼承會帶來可維護性的問題
⇒ B 繼承 A -> B is A,這是非常強的耦合 所以 Liskov 才會建議 所有用 A 的地方一定要可以用 B

在考慮代碼複用上,優先考慮組合,如果真的要多態,使用 interface 跟 abstract class 會是更好的組合

擴展闡述

所有對某個介面 (interface / api)的實現,都可以是作為對該介面的 subtyping,無論介面背後的實現更動,行為應該保持跟當初承諾一致

  • api 會依照 client 版號產生不同的行為,這會破壞當初的承諾

Interface Segregation

Robert Martin 參與印表機開發,處理/印出/裝訂都會產生一個對應的 job 類別,隨著開發規模,Job 內有上百個 function 與需求,所有的業務邏輯都引用了 Job,到後面 Job 類別的 typo 修正,都會導致專案數個小時的編譯時間

開發只改列印相關的功能,但他不知道其他人有沒有依賴這個方法,應該要具體功能類與 Job 類透過介面分離,功能類只依賴介面 API,讓實際的 Job 類工作避免互相干擾

當一個功能豐富的 concrete class 要給多個調用方提供功能,應該透過介面或抽象類來提供給 不同調用方不同的功能夠過不同的介面隔離 ⇒ SRP 指導組件的設計,ISP 用於指導介面的設計

不應讓客戶端知道額外的功能

總結

  • SRP 是軟體架構的理念前提,一個組件不應該被多個不相關的業務而造成耦合帶來維護性上的問題
  • OCP 則是設計原則的指導思想,可以透過設計模式、DIP 來達成
  • DIP 描述組件之間如何抽象化與組織的指導方針
  • LSP 確保介面實現與使用方的耦合,保證介面行為的穩定
  • ISP 維護與使用一個組件該暴露的知識,在實作上指導介面設計
    • 當一個介面因為業務 A 而修改,那應該也只有業務 A 被影響到

Categories