物件導向程式設計 - SOLID 設計原則 : SRP、OCP、LSP、ISP、DIP
物件導向程式設計 - SOLID 設計原則 : SRP、OCP、LSP、ISP、DIP
程式設計的武功心法
目錄
- 前言 : 軟體的價值
- SRP : 單一職責原則
- OCP : 開放 - 封閉原則
- LSP : 里氏替換原則
- ISP : 介面隔離原則
- DIP : 依賴反向原則
- 設計原則 : 分類排序
前言 : 軟體的價值
軟體提供的價值有兩種 :
- 第一種 : 讓電腦的行為「符合需求」
- 第二種 : 讓電腦的行為可以「輕易改變」
軟體 (Software)
Soft - 軟的、可以輕易改變的
Ware - 產品
第一種就是讓程式能動,但第二種要如何實現 ?
輕易改變是一個抽象的概念,具體的描述可以理解為 :
如何讓建構產品的「程式碼」更容易的、閱讀、維護與擴充。
SOLID 設計原則
實現需求的武功心法
由五種原則的字母 組成 SOLID (堅硬的) 單字的排序 :
- SRP - 單一職責原則
- OCP - 開放 - 封閉原則
- LSP - 里氏替換原則
- ISP - 介面隔離原則
- DIP - 依賴反向原則
如果你在網路上搜尋過「軟體 設計原則」還會發現有六大、七大或九大原則:
它們通常已經包含這五項。
用途
維基百科
存在的目的是為了建置清晰、可讀與可延伸的開發指南,
並且還可以應用在 測試驅動開發、敏捷開發,以及自適應軟體開發的基本原則。
我認為這五項,實際上就是很多軟體設計方法論的根源點。
只要完全遵守,即便沒有任何的架構,也能夠將程式寫得井然有序、條條有理。
SRP : 單一職責原則
誤解
看到這個名字,直覺想到的是「一個函式只做一件事」或「一個類別只做一種事」。
這樣的理解不太精確,因為這個是「重構的原則」並不是 SRP。
定義
一個模組應該有一個且只有一個理由會使其改變
更容易理解的描述 :
一個模組應該只對唯一的一個角色負責
- 模組: 指的是原始檔 或者 類別
- 角色: 指的是特定群體的使用者
- 理由: 指的則是這個群體的需求
合併起來重新描述
一個原始檔案的類別只會對系統中特定角色的使用者負責,
只有當這個特定群體的需求改變,程式碼才會改變。
從這一段描述可以知道 :
單一職責原則,實際上是一種「分類」的方法,依據的是「不同角色的使用者」(變化)。
為什麼 ?
一個簡單的理解
程式之所以會修改,通常是使用者需求的改變。
假設 : 一個類別只做一種事,但這個事剛好被兩個部門的角色使用到
當其中一個部門提出新的需求,調整時就會影響到另外一個
雖然該部門提升業務能力,但另外一個部門卻得為他們的收益付出代價。
(被影響的一方,基本上都無法接受。)
更好的情況
建置時分別獨立,各自對自己的使用者負責。
以開發者的角度來看:
雖然會導致程式碼重複,但複製代碼的成本絕對會比多個角色調整、驗證
與驗證遺漏,導致錯誤的影響付出較少的代價。
這個原則出現的原因是由於「Conway 定律」的積極推論
Conway 定律說的是 :
軟體產品的架構與專案團隊的組織結構是互相影響的
Conway 定律,積極推論 :
軟體系統的最佳結構 深深受到使用它的組織社會結構所影響
也就是說 :
組織架構通常也是軟體架構的最佳參照。
除非組織的部門真的有共用到某些資源,否則我們開發的程式,就不應該將不同角色的程式模組重複使用。
怎麼做 ?
DDD 領域驅動設計的分層結構,可以很好的實現
- 不同的角色 : 映射為領域層(Domain Layer),不同的業務。
- 該業務的領域服務,各自使用自己的 實體(Entity) 與值對象(VO)
- 跨部門協作 : 由處理「流程」的應用層(Application Layer)負責。
- 共用的資源 : 放入到基礎設施層(Infrastructure Layer)
在各個層級與區塊中都有一個單一負責的對象,因此符合單一職責原則。
OCP : 開放 - 封閉原則
定義
一個軟體製品應該對於擴展是開放的 但對於修改是封閉的
話句話說 : 如果今天在建一棟樓
一樓已經完成,追加新的設計,只能從二樓開始。
不應為了某種需求 在一樓的牆壁鑽孔、打洞。
萬一剛好是某個重要結構,可能導致傾斜或倒塌,這樣肯定得不償失。
軟體設計的架構
與其說要遵循這個原則,不如說是要「設計」成符合這個原則的系統。
同樣以建樓為例 :
一樓再搭建時,就設想還會添加哪些設備
- 有採光的需求 -> 預留窗戶的空間
- 有冷氣的需求 -> 規劃陽台與室內機的動線
為什麼 ?
開頭提到過的軟體第二種價值 :「讓電腦的行為可以輕易改變」。
或具體說法建構,更容易閱讀、維護與擴充的產品程式碼。
「開放 - 封閉原則」 就是一個實際的【指導方針】
理想狀態,在擴展新功能時,修改舊程式的數量,無限趨近於零。
怎麼做 ?
原則的描述,它是一個「大原則」只指引方向。
具體的作法 :
- 單一職責原則
- 依賴反向原則 (之後提到)
將「重要」不可輕易改變的模組,保護起來避免外部修改。
將「動態」需要時常變更的模組,保留空間提供後續調整。
DDD 領域驅動設計的架構
領域層通常就是業務的核心邏輯是組織創造收益的根本原因,不可能經常更動。
(會變更的情況 通常都是組織經過重大調整。)
所以功能擴充會在「領域層」添加新的「業務區塊」,然後才在「容易變動的應用層與使用者介面層」,調整服務的項目清單。
LSP : 里氏替換原則
定義
「里氏」是美國計算機科學家 - Barbarra Liskov
她於 1988 年,寫下定義子型態的方式 :
這裡需要如下的替換性質 :
若對於型態 S 每個物件 o1 都存在一個型態為 T 的物件 o2,
使得在所有針對 T 編寫的程式 P 中,用 o1 替代 o2 後 ,程式 P 的行為功能不變,則 S 是 T 的子型態。
這一段描述,使用許多變數。
為了更好的理解,可以先關注「子型態」與「替換」這兩個關鍵詞。
- 子型態 : 說的是物件導向中,繼承的關係
- 替換 : 則是繼承關係的一種使用方式
例如 :
- 應用程式呼叫「授權介面」中計算費用的方法
- 授權介面分別由個人授權與企業授權「繼承實作」
應用程式不需要依賴兩個子型態類別的任何一種,兩種子型態的授權又都可以替換成授權介面的物件。
該範例符合里氏替換原則
- 型態 S : 個人授權與企業授權
- 型態 T : 授權介面
- 程式 P : 應用程式
用個人或企業授權的物件,替代授權介面的物件功能不變
為什麼 ?
首先,父類別-抽象介面,存在的目的是什麼 ?
維基百科,里氏替換原則的相關連結: 「契約式設計」
契約式設計
要求軟體設計者必須為軟體組件定義正式的、精確的並且可驗證的介面。
也就是說:
介面存在的目的,就像是一種契約,用來驗證實作提供的東西到底府不符合需求。
不驗證沒有契約,但拿到實際且正確的東西,當然沒有問題。 (替代)
但假設 : 已經簽好契約
廠商卻發給我另外一種東西,還強迫必須接受
對應到程式碼 : 負責的模組必須加上各種判斷,來辨別這個東西到底是什麼
在系統中,額外機制就是混亂因子。
將會導致整體架構逐漸失序,使得系統難以維護與擴充更新。
ISP : 介面隔離原則
定義
不應強迫客戶端依賴它不使用的方法
原則的由來
三個使用者,同時使用一個模組,但各自都只有使用其中一個方法。
對於任一使用者來說,模組中另外兩個方法是他不需要的。
所以在使用者與模組之間,又各自新增一個介面 :
該介面只定義使用者會使用的方法,並且隔離彼此。(名稱由來)
背後的羅輯
就是不強迫客戶端依賴它不使用的方法。(客戶端不一定是真實的使用者,有可能是上層模組與下層模組的使用關係。)
為什麼 ?
這個原則,做了兩件事情:
- 將大的模組接口,拆分成許多小的且具體的接口
- 將客戶端模組的依賴,轉移到小的接口
第一個好處
維護客戶端的工程師,可以清楚的知道模組需要的是什麼服務。
(大模組與我之間,關聯並沒有那麼的直接)
第二個好處
客戶端對大模組的依賴解除
解除大模組依賴的好處 ?
大模組的存在是不太正常,但卻又自然而然。
因為程式從一開始創建,並不會立馬就想到未來會有多少功能。
在原本的模組,拓展新功能會是個省時省力的方法。
一次兩次的疊加沒啥問題,但當發現已經有點臃腫時,已經無法捨棄。
如何修復 ?
工程師看見這個問題,回頭修改會牽連太廣、成本太大,而且也會違反「開放 - 封閉原則」。
更好的做法是為未來做準備
如果有個全新且更精準的模組替代,對於客戶端與依賴的小接口,基本上什麼都不用做。
因為是新模組依賴於我的小接口,而不是我客戶端模組還要改動程式碼去依賴新模組。
DIP : 依賴反向原則
定義
高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面。
- 高層次的模組 : 指的是核心的業務規則
- 低層次的模組 : 指的是非核心的工具、介面或資料庫
傳統的應用程式架構是高層次透過低層次實現功能
例如 :
將分析報告保存在系統中,是分析的模組透過資料庫的模組使用儲存的功能。
依據原則
兩者的依賴關係要調整成分析報告使用抽象介面的保存方法,然後資料庫在依據抽象介面的定義,實作資料庫的儲存功能。
為什麼 ?
高層次為什麼是高層次 ?
因為它是企業「創造收益」的核心規則,即便沒有系統,使用紙筆作業也依然成立。
因此,不能隨意變動應該被保護起來
如果,依賴於低層次模組
代表低層次模組變動會回頭影響到高層次模組。嚴重一點出現異常,更可能導致高層次模組無法作業。
穩妥起見 : 解開依賴關係
即便低層次模組發生問題,最多也只是無法儲存,不會導致高層次模組業務停擺。
為什麼依賴於抽象介面?
為了可以拓展新的功能
就像前面提到過的抽象介面是契約精神的展現,我要的東西規格已經定義清楚。
但如果有更好的方法,就是額外實作新的功能,將原本舊的模組替換掉即可。
設計原則 : 分類排序
個人理解 : 分成三部分
上述的 SOLID 設計原則的描述順序,應該多少會感到有些混亂。
這是因為 SOLID 只是單字字母的排序,並不是重要性或者因果推演的排序。
我認為可以分類成三個部分:
第一個部分 : 開放 - 封閉原則
它是一個大方向原則,總體目標就是將系統設計成「容易拓展新的,並且少量修改舊的」。
第二個部分 : 單一職責原則
講的是「分類」的方法,可以運用在「開放 - 封閉原則」,「封閉」的部分,
透過需求根源點 - 「角色的分類」,將可能會修改的部分集中。
第三個部分 : 依賴反轉原則、里氏替換原則、介面隔離原則
講的是介面的使用方式,可以運用在「開放 - 封閉原則」,「開放」的部分。
依賴反轉原則 :
告訴你使用抽象介面,可以在保證核心正常運作的情況下,還能夠拓展新功能。
里氏替換原則 :
介面與類別 - 一對多關係
它可以幫助系統,在拓展功能時,保證子型態模組的可靠性。
介面隔離原則 :
介面與類別 - 多對一關係
它可以幫助系統,無法分割類別時,保證拓展功能的純粹性,使得模組具有高內聚與可讀性。
就像是「雅量」
我在搜尋相關資訊時,總覺得五項原則,就像是 :
一千個讀者心中,有一千個哈姆雷特。
不是原則嗎 ?
怎麼講的都不太一樣 !?
如果上述有錯或者跟你想的有出入,都可以留言討論。
參考資料
- 書籍 : Clean Architecture - 無瑕的程式碼 , 整潔的軟體設計與架構篇
留言
張貼留言