開放封閉原則 (Open-Close Principle)

軟體實體(類別、模組、函式等等)應該對擴展開放,而對修改封閉

在我們了解什麼是「對擴展開放」和「對修改封閉」之前,先讓我們談談:什麼是擴展,什麼又是修改呢?

用白話一點的方式來形容,修改就是把東西拆開來改,像是手術;而擴展就是對東西額外加裝模組,像是添購設備。我們用飛行來舉例,像是鳥類直接用翅膀飛行,如果有需要修改飛行方法的話就得對鳥直接進行手術;但如果今天是一個裝備了噴射背包的人,我們只需要把噴射背包換成噴射鞋子、甚至噴射翅膀就可以了,不需要去修改人這個本體。

這邊可以發現開放封閉原則是針對「改變的時候」去做一個行動的建議,例如需求追加和變更等等。凡是變化都有成本,例如變動的難易度、變動造成的影響範圍等等都會影響到成本,若是程式碼冗長、內部邏輯複雜,類別之間互相耦合、影響範圍很廣,導致綁手綁腳或壞東壞西等等狀況,使得修改很困難,成本就會變高,進而使得開發效率變低。

然而,軟體並不是製造完畢就完工的東西,而是隨需求而生、隨需求而變的動態作品,因此程式碼的修改或重構相當頻繁。就像我們在 內聚耦合篇 提過的:軟體面對改變的能力,就像基因適應環境並生存下去的能力。因此,程式必須具有彈性,也就是需要盡可能降低修改的成本。

那麼讓我們回到前面:動手術跟換道具,哪個的成本比較高呢?

面對需求,對程式碼的改動是透過增加新程式碼進行的,而不是更改現有的程式碼  
(《大話設計模式》)

所以,我們希望能夠用擴充的方式去完成變化,而不是用針對內部進行修改的方式來做;希望藉由良好的設計,能迴避上面那串修改困難導致成本高昂的問題。

而這個思路,其實你我都已經很習以為常了,就是模組化

模組化

組裝電腦的時候,就是針對主機板加裝各種模組;寫程式的時候,我們也很習慣引入套件來使用;甚至使用 Visual Studio 或是 Chrome 這類軟體的時候,我們也都會使用擴充套件來加上我們需要的功能。甚至洛克人也是,我們打倒 BOSS 之後就能替洛克人加裝各種模組,讓他能夠具備各個 BOSS 的功能。噢當然我們不會把整台洛克人拆開然後改造成另一台,不然拆來拆去多麻煩。

我們的周遭四處可見模組化,用模組來擴充本體的想法自古以來比比皆然。

這邊就可以發現到:主機板上面事先會留好許多讓你接顯示卡或記憶體等等的插槽、Chrome 這類軟體會開放 API 和權限等功能給擴充套件來使用。當我們想要利用擴展的方式來擴充本體的能力時,我們需要留下一個供對接的地方,也就是擴充點

擴充點可以說是留給後人的貼心禮物(跟另一種留給後人的炸死人禮物並不相同)但是,我們要怎麼知道哪些地方可以擴充、可能擴充呢?我們可以先區分主要邏輯附加邏輯,像是洛克人跟技能,你和噴射背包,又或者是「查詢客戶」的主邏輯和各種不同「查詢客戶的條件」等等的組合。

因為如果不加以區分,我們就沒辦法把附加邏輯做成模組,也就找不到主要邏輯和附加邏輯之間的擴充點,如此一來就勢必要針對混成一坨的邏輯做修改和業務處理,接著就會落入我們在上一篇 單一職責原則 提過的各種悲慘下場:修改一個地方影響一狗票功能、修改前必須痛苦地閱讀大量不相關的程式碼…等等。

這些問題,也正是單一職責原則所要解決的。此處可以直接參考上一篇也引用過的 再談物件導向設計原則: 單一職責原則,定義、解析與實踐 這篇,裡面的學生列表例子就蠻直接好懂的。

另外也必須推薦一下這位大大的 物件導向設計原則:開放封閉原則,定義、解析與實踐 ,對業務邏輯和附加邏輯的說明也相當明確,從為什麼要隔離兩者,到如何實踐都有說明,值得一看。

而我們辨認出主要邏輯跟附加邏輯之後,該怎麼實行開放封閉原則呢?

實行

答案就是抽象。(《無瑕的程式碼:敏捷完整篇》)

這邊舉幾個方向:我們可以在主要邏輯和附加邏輯之間,加入抽象層來解耦合,也就是我們 介面 大哥該出場的時候了。當我們的類別不再堅持依賴某個物件,例如說我就是要噴射背包,然後把背包黏死在背後;而是接受我只需要能飛的東西,不論傳遞進來的是噴射背包還是噴射鞋子,如此一來就夠用介面表達出需求,使得功能可以被任何符合需求的方式擴展

另外還有,使用外部注入來處理附加邏輯。除了不將附加邏輯寫在類別中,降低修改的機會以外,和介面的邏輯一致:你給什麼工具我就用什麼工具。當我們的附加邏輯是從外部丟給類別,使得類別預先留好擴充點,並且能由外部決定擴充方式,要擴展也就相當容易了。順便一提,我進公司學習以來,注入跟介面通常都是一起出現的 Combo 技。

當我們在設置我們的擴充點(上個世紀稱作「放置鉤子」)時,有時會預測失敗,變成不必要的複雜性。也很容易走火入魔,就變成過度設計。因此,我們最終會等到足夠確信將會變化時,才進行重構的動作。

在無瑕的程式碼中,建議可以接受「被愚弄一次」,先假設不會變化,而當真的變化到來時,就將該變化相關的部份重構抽象起來,得了一次病,從此就免疫,還可以少走冤枉路。又或許,也可以嘗試看看 三次原則

2024.09 補充:
看見 Huanlin 大大寫了這篇 避免過早的抽象設計,覺得和「被愚弄一次」的精神相似。最近也在體會過早抽象帶來的痛苦,決定把這篇補充回來,給正在認識 OCP 的朋朋們參考。

OCP 能幫助我們思考並寫出乾淨彈性的程式碼,但切記不要走火入魔,否則反而會帶來更多維護的成本。謹記三次重構原則,保一世平安,阿彌陀佛。

結語

最後,開放封閉原則的範圍實在是太大了。事實上,其他設計原則,例如單一職責、依賴反轉等等,都是為了達到開放封閉這個終極的目標而產生的。但是,我們不可能預測到所有變化,也沒有任何做法能夠適用於所有狀況,因此要達到完全的封閉是不可能的。然而,這是我們應當嘗試精進的目標,只要謹記開放封閉原則,就能不斷改善架構,也就離良好的設計更進一步了。

而對我而言,開放封閉的好處在於強迫像我這樣的工程師去思考:哪些地方是附加邏輯,哪些地方可以留作擴充,又該怎麼做才能方便擴充,這個過程和嘗試對我輩菜鳥才是真正最有價值的地方吧。

最後感謝一下 Ray 大大的路過指點。我當時問了不知道怎麼形容開放封閉原則,大大就說了個例子:咱們人哪,學新東西可是比改個性來得簡單多了。也基於這個例子讓我想到了手術和洛克人,還有變動的難易度、本性(核心邏輯)和新技能(附加邏輯)的差別可以這樣咻咻地串起來,這邊就謝過啦。順便也貼下大大的 Blog,加減蹭一下。

本系列下一篇:菜雞與物件導向 (12): 里氏替換原則

參考資料

同系列文章