iT邦幫忙

3

【程式如何正確撰寫 ?】物件導向程式設計 - SOLID 設計原則 : SRP、OCP、LSP、ISP、DIP

程式設計的武功心法

目錄

  • 前言 : 軟體的價值
  • SRP : 單一職責原則
  • OCP : 開放 - 封閉原則
  • LSP : 里氏替換原則
  • ISP : 介面隔離原則
  • DIP : 依賴反向原則
  • 設計原則 : 分類排序

前言 : 軟體的價值

軟體提供的價值有兩種 :

  • 第一種 : 讓電腦的行為「符合需求」
  • 第二種 : 讓電腦的行為可以「輕易改變」

001

軟體 (Software) 
Soft - 軟的、可以輕易改變的
Ware - 產品

第一種就是讓程式能動,但第二種要如何實現 ?

輕易改變是一個抽象的概念,具體的描述可以理解為 :

如何讓建構產品的「程式碼」更容易的、閱讀、維護與擴充。

SOLID 設計原則

實現需求的武功心法

由五種原則的字母 組成 SOLID (堅硬的) 單字的排序 :

  • SRP - 單一職責原則
  • OCP - 開放 - 封閉原則
  • LSP - 里氏替換原則
  • ISP - 介面隔離原則
  • DIP - 依賴反向原則

如果你在網路上搜尋過「軟體 設計原則」還會發現有六大、七大或九大原則:

002
它們通常已經包含這五項。


用途

維基百科

存在的目的是為了建置清晰、可讀與可延伸的開發指南,
並且還可以應用在 測試驅動開發、敏捷開發,以及自適應軟體開發的基本原則。

003

我認為這五項,實際上就是很多軟體設計方法論的根源點。

只要完全遵守,即便沒有任何的架構,也能夠將程式寫得井然有序、條條有理。


SRP : 單一職責原則

誤解

看到這個名字,直覺想到的是「一個函式只做一件事」或「一個類別只做一種事」。

004

這樣的理解不太精確,因為這個是「重構的原則」並不是 SRP。

定義

一個模組應該有一個且只有一個理由會使其改變

更容易理解的描述 :

一個模組應該只對唯一的一個角色負責

005

  • 模組: 指的是原始檔 或者 類別
  • 角色: 指的是特定群體的使用者
  • 理由: 指的則是這個群體的需求

合併起來重新描述

一個原始檔案的類別只會對系統中特定角色的使用者負責,
只有當這個特定群體的需求改變,程式碼才會改變。

從這一段描述可以知道 :

單一職責原則,實際上是一種「分類」的方法,依據的是「不同角色的使用者」(變化)。

006


為什麼 ?

一個簡單的理解

程式之所以會修改,通常是使用者需求的改變。

假設 : 一個類別只做一種事,但這個事剛好被兩個部門的角色使用到

當其中一個部門提出新的需求,調整時就會影響到另外一個

007

雖然該部門提升業務能力,但另外一個部門卻得為他們的收益付出代價。
(被影響的一方,基本上都無法接受。)

更好的情況

建置時分別獨立,各自對自己的使用者負責。

008


以開發者的角度來看:

雖然會導致程式碼重複,但複製代碼的成本絕對會比多個角色調整、驗證 
與驗證遺漏,導致錯誤的影響付出較少的代價。

這個原則出現的原因是由於「Conway 定律」的積極推論

Conway 定律說的是 :

軟體產品的架構與專案團隊的組織結構是互相影響的

Conway 定律,積極推論 :

軟體系統的最佳結構 深深受到使用它的組織社會結構所影響

也就是說 :

組織架構通常也是軟體架構的最佳參照。

除非組織的部門真的有共用到某些資源,否則我們開發的程式,就不應該將不同角色的程式模組重複使用。


怎麼做 ?

DDD 領域驅動設計的分層結構,可以很好的實現

007

  • 不同的角色 : 映射為領域層(Domain Layer),不同的業務。
    • 該業務的領域服務,各自使用自己的 實體(Entity) 與值對象(VO)
  • 跨部門協作 : 由處理「流程」的應用層(Application Layer)負責。
  • 共用的資源 : 放入到基礎設施層(Infrastructure Layer)

在各個層級與區塊中都有一個單一負責的對象,因此符合單一職責原則。


OCP : 開放 - 封閉原則

定義

一個軟體製品應該對於擴展是開放的 但對於修改是封閉的

話句話說 : 如果今天在建一棟樓

一樓已經完成,追加新的設計,只能從二樓開始。
不應為了某種需求 在一樓的牆壁鑽孔、打洞。
萬一剛好是某個重要結構,可能導致傾斜或倒塌,這樣肯定得不償失。

軟體設計的架構

與其說要遵循這個原則,不如說是要「設計」成符合這個原則的系統。

同樣以建樓為例 :

一樓再搭建時,就設想還會添加哪些設備

  • 有採光的需求 -> 預留窗戶的空間
  • 有冷氣的需求 -> 規劃陽台與室內機的動線

為什麼 ?

開頭提到過的軟體第二種價值 :「讓電腦的行為可以輕易改變」。

或具體說法建構,更容易閱讀、維護與擴充的產品程式碼。

「開放 - 封閉原則」 就是一個實際的【指導方針】

理想狀態,在擴展新功能時,修改舊程式的數量,無限趨近於零。

怎麼做 ?

原則的描述,它是一個「大原則」只指引方向。

具體的作法 :

  • 單一職責原則
  • 依賴反向原則 (之後提到)
將「重要」不可輕易改變的模組,保護起來避免外部修改。
將「動態」需要時常變更的模組,保留空間提供後續調整。

DDD 領域驅動設計的架構

領域層通常就是業務的核心邏輯是組織創造收益的根本原因,不可能經常更動。
(會變更的情況 通常都是組織經過重大調整。)

009

所以功能擴充會在「領域層」添加新的「業務區塊」,然後才在「容易變動的應用層與使用者介面層」,調整服務的項目清單。

LSP : 里氏替換原則

定義

「里氏」是美國計算機科學家 - Barbarra Liskov

010

她於 1988 年,寫下定義子型態的方式 :

這裡需要如下的替換性質 :

若對於型態 S 每個物件 o1 都存在一個型態為 T 的物件 o2,
使得在所有針對 T 編寫的程式 P 中,用 o1 替代 o2 後 ,程式 P 的行為功能不變,則 S 是 T 的子型態。

這一段描述,使用許多變數。

為了更好的理解,可以先關注「子型態」與「替換」這兩個關鍵詞。

  • 子型態 : 說的是物件導向中,繼承的關係
  • 替換 : 則是繼承關係的一種使用方式

例如 :

  • 應用程式呼叫「授權介面」中計算費用的方法
  • 授權介面分別由個人授權與企業授權「繼承實作」

011

應用程式不需要依賴兩個子型態類別的任何一種,兩種子型態的授權又都可以替換成授權介面的物件。

該範例符合里氏替換原則

012

  • 型態 S : 個人授權與企業授權
  • 型態 T : 授權介面
  • 程式 P : 應用程式

用個人或企業授權的物件,替代授權介面的物件功能不變


為什麼 ?

首先,父類別-抽象介面,存在的目的是什麼 ?

維基百科,里氏替換原則的相關連結: 「契約式設計」

013


契約式設計

014
要求軟體設計者必須為軟體組件定義正式的、精確的並且可驗證的介面。

也就是說:

介面存在的目的,就像是一種契約,用來驗證實作提供的東西到底府不符合需求。

015

不驗證沒有契約,但拿到實際且正確的東西,當然沒有問題。 (替代)

但假設 : 已經簽好契約

廠商卻發給我另外一種東西,還強迫必須接受

對應到程式碼 : 負責的模組必須加上各種判斷,來辨別這個東西到底是什麼

016

在系統中,額外機制就是混亂因子。
將會導致整體架構逐漸失序,使得系統難以維護與擴充更新。

ISP : 介面隔離原則

定義

不應強迫客戶端依賴它不使用的方法

原則的由來

三個使用者,同時使用一個模組,但各自都只有使用其中一個方法。

017

對於任一使用者來說,模組中另外兩個方法是他不需要的。

所以在使用者與模組之間,又各自新增一個介面 :

018

該介面只定義使用者會使用的方法,並且隔離彼此。(名稱由來)

背後的羅輯

就是不強迫客戶端依賴它不使用的方法。(客戶端不一定是真實的使用者,有可能是上層模組與下層模組的使用關係。)

為什麼 ?

這個原則,做了兩件事情:

  1. 將大的模組接口,拆分成許多小的且具體的接口
  2. 將客戶端模組的依賴,轉移到小的接口

第一個好處

維護客戶端的工程師,可以清楚的知道模組需要的是什麼服務。
(大模組與我之間,關聯並沒有那麼的直接)

第二個好處

客戶端對大模組的依賴解除

解除大模組依賴的好處 ?

大模組的存在是不太正常,但卻又自然而然。
因為程式從一開始創建,並不會立馬就想到未來會有多少功能。

在原本的模組,拓展新功能會是個省時省力的方法。
一次兩次的疊加沒啥問題,但當發現已經有點臃腫時,已經無法捨棄。

如何修復 ?

工程師看見這個問題,回頭修改會牽連太廣、成本太大,而且也會違反「開放 - 封閉原則」。

019


更好的做法是為未來做準備

如果有個全新且更精準的模組替代,對於客戶端與依賴的小接口,基本上什麼都不用做。
因為是新模組依賴於我的小接口,而不是我客戶端模組還要改動程式碼去依賴新模組。

020


DIP : 依賴反向原則

定義

高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面。
  • 高層次的模組 : 指的是核心的業務規則
  • 低層次的模組 : 指的是非核心的工具、介面或資料庫

傳統的應用程式架構是高層次透過低層次實現功能

例如 :

將分析報告保存在系統中,是分析的模組透過資料庫的模組使用儲存的功能。

021


依據原則

兩者的依賴關係要調整成分析報告使用抽象介面的保存方法,然後資料庫在依據抽象介面的定義,實作資料庫的儲存功能。

022


為什麼 ?

高層次為什麼是高層次 ?

因為它是企業「創造收益」的核心規則,即便沒有系統,使用紙筆作業也依然成立。

因此,不能隨意變動應該被保護起來

023

如果,依賴於低層次模組

 代表低層次模組變動會回頭影響到高層次模組。嚴重一點出現異常,更可能導致高層次模組無法作業。

024


穩妥起見 : 解開依賴關係

即便低層次模組發生問題,最多也只是無法儲存,不會導致高層次模組業務停擺。

025


為什麼依賴於抽象介面?

為了可以拓展新的功能

026

就像前面提到過的抽象介面是契約精神的展現,我要的東西規格已經定義清楚。
但如果有更好的方法,就是額外實作新的功能,將原本舊的模組替換掉即可。

設計原則 : 分類排序

個人理解 : 分成三部分

上述的 SOLID 設計原則的描述順序,應該多少會感到有些混亂。

 這是因為 SOLID 只是單字字母的排序,並不是重要性或者因果推演的排序。

我認為可以分類成三個部分:

027

第一個部分 : 開放 - 封閉原則

它是一個大方向原則,總體目標就是將系統設計成「容易拓展新的,並且少量修改舊的」。

第二個部分 : 單一職責原則

講的是「分類」的方法,可以運用在「開放 - 封閉原則」,「封閉」的部分,
透過需求根源點 - 「角色的分類」,將可能會修改的部分集中。

第三個部分 : 依賴反轉原則、里氏替換原則、介面隔離原則

講的是介面的使用方式,可以運用在「開放 - 封閉原則」,「開放」的部分。

依賴反轉原則 :

告訴你使用抽象介面,可以在保證核心正常運作的情況下,還能夠拓展新功能。

028


里氏替換原則 :

介面與類別 - 一對多關係
它可以幫助系統,在拓展功能時,保證子型態模組的可靠性。

029


介面隔離原則 :

介面與類別 - 多對一關係
它可以幫助系統,無法分割類別時,保證拓展功能的純粹性,使得模組具有高內聚與可讀性。

030


就像是「雅量」

我在搜尋相關資訊時,總覺得五項原則,就像是 :

一千個讀者心中,有一千個哈姆雷特。

031

不是原則嗎 ?

怎麼講的都不太一樣 !?

032

如果上述有錯或者跟你想的有出入,都可以留言討論。

參考資料

  • 書籍 : Clean Architecture - 無瑕的程式碼 , 整潔的軟體設計與架構篇

尚未有邦友留言

立即登入留言