菜雞與物件導向 (11): 開放封閉原則
開放封閉原則 (Open-Close Principle)
軟體實體(類別、模組、函式等等)應該對擴展開放,而對修改封閉
在我們了解什麼是「對擴展開放」和「對修改封閉」之前,先讓我們談談:什麼是擴展,什麼又是修改呢?
用白話一點的方式來形容,修改就是把東西拆開來改,像是手術;而擴展就是對東西額外加裝模組,像是添購設備。我們用飛行來舉例,像是鳥類直接用翅膀飛行,如果有需要修改飛行方法的話就得對鳥直接進行手術;但如果今天是一個裝備了噴射背包的人,我們只需要把噴射背包換成噴射鞋子、甚至噴射翅膀就可以了,不需要去修改人這個本體。
這邊可以發現開放封閉原則是針對「改變的時候」去做一個行動的建議,例如需求追加和變更等等。凡是變化都有成本,例如變動的難易度、變動造成的影響範圍等等都會影響到成本,若是程式碼冗長、內部邏輯複雜,類別之間互相耦合、影響範圍很廣,導致綁手綁腳或壞東壞西等等狀況,使得修改很困難,成本就會變高,進而使得開發效率變低。
然而,軟體並不是製造完畢就完工的東西,而是隨需求而生、隨需求而變的動態作品,因此程式碼的修改或重構相當頻繁。就像我們在 內聚耦合篇 提過的:軟體面對改變的能力,就像基因適應環境並生存下去的能力。因此,程式必須具有彈性,也就是需要盡可能降低修改的成本。
那麼讓我們回到前面:動手術跟換道具,哪個的成本比較高呢?
面對需求,對程式碼的改動是透過增加新程式碼進行的,而不是更改現有的程式碼
(《大話設計模式》)
所以,我們希望能夠用擴充的方式去完成變化,而不是用針對內部進行修改的方式來做;希望藉由良好的設計,能迴避上面那串修改困難導致成本高昂的問題。
而這個思路,其實你我都已經很習以為常了,就是模組化。
模組化
組裝電腦的時候,就是針對主機板加裝各種模組;寫程式的時候,我們也很習慣引入套件來使用;甚至使用 Visual Studio 或是 Chrome 這類軟體的時候,我們也都會使用擴充套件來加上我們需要的功能。甚至洛克人也是,我們打倒 BOSS 之後就能替洛克人加裝各種模組,讓他能夠具備各個 BOSS 的功能。噢當然我們不會把整台洛克人拆開然後改造成另一台,不然拆來拆去多麻煩。
我們的周遭四處可見模組化,用模組來擴充本體的想法自古以來比比皆然。
這邊就可以發現到:主機板上面事先會留好許多讓你接顯示卡或記憶體等等的插槽、Chrome 這類軟體會開放 API 和權限等功能給擴充套件來使用。當我們想要利用擴展的方式來擴充本體的能力時,我們需要留下一個供對接的地方,也就是擴充點。
擴充點可以說是留給後人的貼心禮物(跟另一種留給後人的炸死人禮物並不相同)但是,我們要怎麼知道哪些地方可以擴充、可能擴充呢?我們可以先區分主要邏輯和附加邏輯,像是洛克人跟技能,你和噴射背包,又或者是「查詢客戶」的主邏輯和各種不同「查詢客戶的條件」等等的組合。
因為如果不加以區分,我們就沒辦法把附加邏輯做成模組,也就找不到主要邏輯和附加邏輯之間的擴充點,如此一來就勢必要針對混成一坨的邏輯做修改和業務處理,接著就會落入我們在上一篇 單一職責原則 提過的各種悲慘下場:修改一個地方影響一狗票功能、修改前必須痛苦地閱讀大量不相關的程式碼…等等。
這些問題,也正是單一職責原則所要解決的。此處可以直接參考上一篇也引用過的 再談物件導向設計原則: 單一職責原則,定義、解析與實踐 這篇,裡面的學生列表例子就蠻直接好懂的。
另外也必須推薦一下這位大大的 物件導向設計原則:開放封閉原則,定義、解析與實踐 ,對業務邏輯和附加邏輯的說明也相當明確,從為什麼要隔離兩者,到如何實踐都有說明,值得一看。
而我們辨認出主要邏輯跟附加邏輯之後,該怎麼實行開放封閉原則呢?
實行
答案就是抽象。(《無瑕的程式碼:敏捷完整篇》)
這邊舉幾個方向:我們可以在主要邏輯和附加邏輯之間,加入抽象層來解耦合,也就是我們 介面 大哥該出場的時候了。當我們的類別不再堅持依賴某個物件,例如說我就是要噴射背包,然後把背包黏死在背後;而是接受我只需要能飛的東西,不論傳遞進來的是噴射背包還是噴射鞋子,如此一來就夠用介面表達出需求,使得功能可以被任何符合需求的方式擴展。
另外還有,使用外部注入來處理附加邏輯。除了不將附加邏輯寫在類別中,降低修改的機會以外,和介面的邏輯一致:你給什麼工具我就用什麼工具。當我們的附加邏輯是從外部丟給類別,使得類別預先留好擴充點,並且能由外部決定擴充方式,要擴展也就相當容易了。順便一提,我進公司學習以來,注入跟介面通常都是一起出現的 Combo 技。
當我們在設置我們的擴充點(上個世紀稱作「放置鉤子」)時,有時會預測失敗,變成不必要的複雜性。也很容易走火入魔,就變成過度設計。因此,我們最終會等到足夠確信將會變化時,才進行重構的動作。
在無瑕的程式碼中,建議可以接受「被愚弄一次」,先假設不會變化,而當真的變化到來時,就將該變化相關的部份重構抽象起來,得了一次病,從此就免疫,還可以少走冤枉路。又或許,也可以嘗試看看 三次原則。
2024.09 補充:
看見 Huanlin 大大寫了這篇 避免過早的抽象設計,覺得和「被愚弄一次」的精神相似。最近也在體會過早抽象帶來的痛苦,決定把這篇補充回來,給正在認識 OCP 的朋朋們參考。OCP 能幫助我們思考並寫出乾淨彈性的程式碼,但切記不要走火入魔,否則反而會帶來更多維護的成本。謹記三次重構原則,保一世平安,阿彌陀佛。
結語
最後,開放封閉原則的範圍實在是太大了。事實上,其他設計原則,例如單一職責、依賴反轉等等,都是為了達到開放封閉這個終極的目標而產生的。但是,我們不可能預測到所有變化,也沒有任何做法能夠適用於所有狀況,因此要達到完全的封閉是不可能的。然而,這是我們應當嘗試精進的目標,只要謹記開放封閉原則,就能不斷改善架構,也就離良好的設計更進一步了。
而對我而言,開放封閉的好處在於強迫像我這樣的工程師去思考:哪些地方是附加邏輯,哪些地方可以留作擴充,又該怎麼做才能方便擴充,這個過程和嘗試對我輩菜鳥才是真正最有價值的地方吧。
最後感謝一下 Ray 大大的路過指點。我當時問了不知道怎麼形容開放封閉原則,大大就說了個例子:咱們人哪,學新東西可是比改個性來得簡單多了。也基於這個例子讓我想到了手術和洛克人,還有變動的難易度、本性(核心邏輯)和新技能(附加邏輯)的差別可以這樣咻咻地串起來,這邊就謝過啦。順便也貼下大大的 Blog,加減蹭一下。
本系列下一篇:菜雞與物件導向 (12): 里氏替換原則
參考資料
- 深入淺出開放封閉原則 Open-Closed Principle - jyt0532’s Blog
- 物件導向設計原則:開放封閉原則,定義、解析與實踐 - WadeHuang的學習迷航記
- 第 10 章 類別 | Clean Code 敏捷軟體開發技巧守則
- 程式設計心法 三次原則(Rule Of Three principle)- 璇之又璇的網路世界
- Object Oriented物件導向設計原則SOLID-2:Open-Close Principle(OCP) 開放封閉原則 - Sian
同系列文章
- 菜雞與物件導向 (0): 前言
- 菜雞與物件導向 (1): 類別、物件
- 菜雞與物件導向 (2): 建構式、多載
- 菜雞與物件導向 (3): 封裝
- 菜雞與物件導向 (4): 繼承
- 菜雞與物件導向 (5): 多型
- 菜雞與物件導向 (6): 抽象、覆寫
- 菜雞與物件導向 (7): 介面
- 菜雞與物件導向 (8): 內聚、耦合
- 菜雞與物件導向 (9): SOLID
- 菜雞與物件導向 (10): 單一職責原則
- 菜雞與物件導向 (11): 開放封閉原則
- 菜雞與物件導向 (12): 里氏替換原則
- 菜雞與物件導向 (13): 介面隔離原則
- 菜雞與物件導向 (14): 依賴反轉原則
- 菜雞與物件導向 (15): 最少知識原則
- 菜雞與物件導向 (Ex1): 小結
哈囉,如果你也有 LikeCoin,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)