面試時被問過「請說明 SOLID 原則」這個問題的人,應該都有過一次吧。
但老實說,就算能把五項原則逐一背出來回答,能接著說「所以要怎麼用在自己的程式碼裡?」的人並不多。
這次不講教科書定義,而是用實務場景拆解 SOLID。內容較多,因此分成上下兩篇。今天先談前面三個字母:SRP、OCP、LSP。
SOLID 是 Robert Martin(Uncle Bob)整理出的物件導向設計五大原則首字母縮寫。
- SRP — 單一職責原則
- OCP — 開放封閉原則
- LSP — Liskov 替換原則
- ISP — 介面隔離原則
- DIP — 相依性反轉原則
五項原則看似各自談不同事情,但貫穿其中的訊息只有一個:發生變更時,縮小需要修改的程式碼範圍。
SRP — 單一職責原則
教科書定義是「類別只能有一項職責」,但 Uncle Bob 本人後來改用更好的說法。
模組只能對單一角色負責。
這裡的角色,是要求變更這段程式碼的人或部門。假設有這樣一個類別。
class Employee {
func calculatePay() { ... } // 會計部門要求修改
func reportHours() { ... } // 人資部門要求修改
func save() { ... } // 開發團隊(DBA)要求修改
}
三個方法的負責人完全不同。如果因應會計部門的需求修改 calculatePay時碰到共用邏輯,人資部門的報表就可能悄悄出錯。實務上發生這種事故時,也很難找出原因。
SRP 將這件事整理成:「要求修改的人不同,程式碼也要分開。」把薪資計算、工作報表和儲存分成不同類別後,會計部門的需求就不會影響人資功能。
OCP — 開放封閉原則
定義是「對擴充開放,對修改封閉」。第一次聽起來像禪問答,但用程式碼來看就很清楚。
// 每次增加付款方式都必須修改這個函式
func pay(method: String, amount: Int) {
if method == "card" { ... }
else if method == "kakaopay" { ... }
else if method == "naverpay" { ... }
// 新付款方式 = 修改 else if = 既有程式碼
}
在這種結構中,若要加入 Toss Pay,就必須打開並修改原本運作正常的函式。每次修改都要承擔破壞既有付款方式的風險。
// 只要「新增」付款方式即可的結構
let payments: [String: Payment] = [
"card": CardPayment(),
"kakaopay": KakaoPayment(),
"naverpay": NaverPayment(),
// 加入 Toss Pay = 一行註冊新類別
]
func pay(method: String, amount: Int) {
payments[method]?.process(amount: amount)
}
不碰既有程式碼(對修改封閉),只要加入新類別就能擴充功能(對擴充開放)。訣竅是只在經常變動的地方採用這種結構。全部程式碼都套用的話,就會違反上一篇提到的 YAGNI。
LSP — Liskov 替換原則
名稱看起來最可怕,但意思其實很簡單。
子類別即使直接替換到使用父類別的地方,也不能讓程式壞掉。
最有名的反例就是矩形與正方形問題。若按照「正方形是矩形」的數學常識進行繼承,就會發生這種事。
class Rectangle {
var width = 0
var height = 0
func setWidth(_ w: Int) { width = w }
func setHeight(_ h: Int) { height = h }
}
class Square: Rectangle {
override func setWidth(_ w: Int) { width = w; height = w } // 因為是正方形
override func setHeight(_ h: Int) { width = h; height = h }
}
// 期待矩形的程式碼
func test(_ rect: Rectangle) {
rect.setWidth(5)
rect.setHeight(4)
print(rect.width * rect.height) // 20期待 Square如果是正方形 16
}
放入 Square 的瞬間,「寬 5、高 4 時面積為 20」這個理所當然的期待就被打破了。即使 is-a 關係看似成立,只要破壞行為契約,就不該使用繼承,這就是 LSP。
實務上的訊號是這樣:如果子類別覆寫父類別方法後丟出例外,或留下空實作,那麼這個繼承很可能違反 LSP。
結束上篇
把今天介紹的三項原則各留下一句話吧。
- SRP — 要求修改的人不同,程式碼也要分開。
- OCP — 讓經常變動的地方能以新增而非修改來因應。
- LSP — 子類別不要破壞父類別的契約。若非破壞不可,代表繼承就是錯的。
下一篇會介紹剩下的兩個字母:ISP(介面隔離)與 DIP(相依性反轉)。尤其 DIP 直接連結到現代框架為什麼都使用相依性注入,應該會很有趣。

![[SOLID #1] SOLID 五個字母,別背誦而是理解(SRP・OCP・LSP) 封面圖](/assets/images/posts/7b95a0fb-b586-4f62-b56e-4a8b31a29dfd/1.jpg)