【物件導向】02 - 物件導向設計原則:SOLID

物件導向的目的在於解耦,以便於未來的維護或開發
對於這些設計的心法,我認為:先思考程式/模組未來的變動的可能性,再去拿捏如何切分架構
依照業務邏輯區分模組,進而建立類別,有需要的地方再去SOLID,歸類程式碼

先講解耦這個詞的意思

解耦:解除耦合性的簡稱,代表解除/降低 物件之間互相連動/依賴的關係

物件之間的耦合越高,維護成本越高,因此物件的設計應使類和構件之間的耦合最小,而物件導向的目的就是為了解決這個問題。

五個基本原則 - SOLID

  • S:Single responsibility principle(SRP) 單一職責
  • O:Open-Closed Principle (OCP) 開放封閉
  • I:Liskov Substitution Principle (LSP) 里氏替換原則
  • L:Interface Segregation Principle (ISP) 介面分隔原則
  • D:Dependency-Inversion Principle (DIP) 依賴反轉

S: Single responsibility principle(SRP) 單一職責

一個模組應該只對一個角色(行為)而改變

  1. 一個類別或一個方法,只處理一種業務邏輯
    例如:一個名為SendMail 的方法,裡面只做寄信動作;一個名為PdfService 的類別,裡面只提供操作PDF相關的功能
  2. 把提供相關功能的程式碼凝聚在一起,形成模組
    例如:"處理訂單"跟"寄信"功能實作在不同的類別(Service)。
  3. 分離不同模組(角色) 使用的程式碼,以免修改程式之後因為耦合造成非預期的錯誤。
    例如:兩個模組共用到的程式,因為某一個模組的需求而修改時,將其共用程式碼分離。

補充:白話一點就是每個模組、類別、函數,都應該只負責的一種功能。|
SRP的難點的就是切分的拿捏,要注意切分不要過細,以免造成開發過於複雜
如同最前面說的:先思考程式/模組未來的變動的可能性,再去拿捏要不要切分架構

References:
https://skyyen999.gitbooks.io/-study-design-pattern-in-java/content/oodPrinciple.html
https://www.jyt0532.com/2020/03/18/srp/
https://ithelp.ithome.com.tw/articles/10191955
https://medium.com/程式愛好者/使人瘋狂的-solid-原則-單一職責原則-single-responsibility-principle-c2c4bd9b4e79 

O: Open-Closed Principle (OCP) 開放封閉

透過結構的設計,使其能透過新增程式來變更系統功能或行為,而不需要修改原有的程式
系統架構會以階層來做分離,使較高階層(核心)部分不會因為低階層(外圍)的修改而受到影響

  1. 透過撰寫新程式碼,以新增功能
  2. 當元件A 需要用到原有元件B 的程式,則使用介面注入B 元件,保護B 元件原有邏輯以免受到影響
    此時元件關係為A 依賴於B 的單向依賴,故A 的修改不會異動到B 的原有程式

補充:開放(抽象類別方法、介面、泛型的)擴增,封閉(處理流程邏輯的)修改。
這篇文章舉出的範例讓我受益良多:https://ithelp.ithome.com.tw/articles/10229362
先劃清主要邏輯額外邏輯的差別,在實體上製作接口,把變動的地方利用模組化隔離開來。
如此一來主要邏輯的演算法會固定 (封閉),新的需求或變動可以透過接口來擴展功能 (開放),利用隔離的介面去實作差異。
 故基本上會延伸利用到 SRP、DIP(依賴反轉) 等概念。

References:
https://www.jyt0532.com/2020/03/19/ocp/
https://medium.com/@f40507777/%E9%96%8B%E6%94%BE%E5%B0%81%E9%96%89%E5%8E%9F%E5%89%87-open-closed-principle-31d61f9d37a5
https://medium.com/程式愛好者/使人瘋狂的-solid-原則-開放封閉原則-open-closed-principle-f7eaf921eb9c
https://igouist.github.io/post/2020/10/oo-11-open-closed-principle/

L: Liskov Substitution Principle (LSP) 里氏替換原則

若B 是A 的子型態,則B 要能完全替代A 的功能。
故將父型態替換成子型態後,原有的程式不需改變,也不會發生任何錯誤或異常。

  1. 先驗條件不可以強化:輸入放寬,參數或限制不應該比父類別多
    例如:父型態要求輸入長度最長50 的字串,則子型態的要求長度不能小於50。否則替換成子型態後,原本的合法輸入會產生錯誤結果
  2. 後驗條件不可以弱化:輸出緊縮,回饋不應該比父型態少
    例如:四輪跟二輪都是"汽車",父型態輸出產品"二輪",則子型態的輸出不能放寬為"汽車"
    否則替換成子型態後,原本接收"二輪"的地方拿到"四輪"會產生錯誤結果

補充:原本的LSP 只關心繼承問題,後來衍生到介面及實作也適用此原則。
基於LSP原則來看,繼承時應該要思考這個子類別能不能做到父類別期望的行為,不要因為單純為了reuse 而繼承。

References:
http://blog.ryantseng.me/2017/10/16/S-O-L-I-D-%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E8%A8%AD%E8%A8%88%E5%8E%9F%E5%89%87-LSP-Liskov-Substitution-Principle/
https://twosheng.com/php%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91-%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8F%9Bliskov-substitution-principle-lsp/
https://igouist.github.io/post/2020/07/oo-4-inheritance/
https://medium.com/@f40507777/%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8F%9B%E5%8E%9F%E5%89%87-liskov-substitution-principle-adc1650ada53

I: Interface Segregation Principle (ISP) 介面分隔原則

模組之間的依賴,只能看見及提供需要的功能

  1. 模組依照不同的Client所需的功能,分別建立不同的介面,以免Client 呼叫到錯誤的功能
    例如:模組有1,2,3 個功能,第一個包含功能1, 2 的介面給Client 1使用,第二個包含功能2, 3 的介面給Client 2使用
  2. 如果實作介面時發生空實作,就代表違反LSP,而同時也違反了ISP
  3. 當依賴到不需要的介面方法,就很可能是因為該界面有太多職責,違反了SRP,而同時也違反了ISP

補充:可以利用SRP、LSP 輔助檢視該介面是不是有符合ISP

References:
https://twosheng.com/php%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91-%E4%BB%8B%E9%9D%A2%E9%9A%94%E9%9B%A2interface-segregation-principle-isp/
https://igouist.github.io/post/2020/11/oo-13-interface-segregation-principle/
https://medium.com/程式愛好者/使人瘋狂的-solid-原則-介面隔離原則-interface-segregation-principle-50f54473c79e

D: Dependency-Inversion Principle (DIP) 依賴反轉

這個原則有兩個觀念:依賴注入(Dependency Injection: 簡稱DI)、控制反轉(Inversion of Control: 簡稱Ioc),因為兩者需要互相配合故合併成一個原則。

"解除呼叫 與被呼叫者 的耦合關係,使高階模組不再直接依賴低階模組。相依於抽象或介面。而不相依於細節。"

控制反轉作用在第四步驟,依賴注入作用在第五步驟,

  1. 當高階模組需要低階模組的功能,依照需求新增介面。
  2. 高階模組從 建立低階模組實體並呼叫 改為 呼叫介面
  3. 低階模組實作介面,可以同時有多個低階模組實作。
  4. 建立一個控制反轉中心(composition root),從外部(通常是程式啟動時) 每個介面要實體化哪一個低階模組。這個步驟又稱為註冊。
  5. 高階模組選擇取得低階模組的方式,有:從建構子的"建構式注入"、從方法參數的"方法注入"、用Get/Set 的"屬性注入"。
    從控制反轉中心拿到實體。

補充:

  1. 高低階兩者之間使用介面解除依賴關係。只要有繼承並實作介面的類別,都可以使用。
    而讓高階的程式碼不用因為需求的變動而做修改。
  2. 原本是高階模組直接依賴低階模組,在高階內實體化低階。控制行為在高階模組中決定。
    此時若要替換這個低階模組,由於此低階模組可能在系統很多地方都有應用到,因替換造成的修改可能會很大。
    控制反轉就是先讓高低階都依賴介面,只要在ioc 容器修改註冊規則即可。
    此時控制行為就被反轉了。
  3. 沒有一定的注入方式,依據系統設計與低階模組實體化的政策,選擇最適合的注入方法吧。

References:
https://twosheng.com/php%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91-%E4%BE%9D%E8%B3%B4%E5%8F%8D%E8%BD%89dependency-inversion-principle-dip/
https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/315490/
https://igouist.github.io/post/2020/12/oo-14-dependency-inversion-principle/

後記(心得)

首尾呼應,物件導向技巧這麼複雜做了這麼多事情。
目的就是希望修改最小化,影響最大化。不要引起不必要或非預期的錯誤。
這些技巧可以應用,但是也不要用過頭,反而造成開發的困擾。

不過技術就是要多學,武器多了,上戰場才不會只有一把匕首可以用。