Skip to content

SOLID 設計原則 (物件導向程式設計)

這是 物件導向程式設計 (OOP) 的五個基本設計原則。當原則一起應用時,可以讓程式更容易維護與擴充,在重構時代碼更清晰。

核心原則

字首縮寫名稱概念
SRP單一功能原則一個功能物件,應該僅有 單一功能
OCP開閉原則軟體應該對擴充開放,對於修改封閉
LSP里氏替換原則子類別 可以擴充 父類別 ,但不能改變 父類別 的功能。
ISP介面隔離原則提供不同的接口介面,給不同的需求使用;不使用單一介面,提供綜合功能。
DIP依賴反轉原則避免 高層模組低層功能 直接耦合關係,需透過 抽象介面 來橋接。

SRP 單一功能原則

一個功能只做一件事,減少功能的複雜度,避免修改功能時互相影響。

多重功能

實作一個發送 email 的功能,當客戶 存在 就會打發送 emailapi 且發送完還要後續處理顯示 (成功/失敗),所有功能寫在同一個函式。

js
function sendEmailToActiveClients(clients) {
  clients.forEach((client) => {
    if (client.isActive) axios.post('send-email-api-url', data)
  })
}

會發現任何一個判斷或功能修改,都要進來這個函式內,且會互相影響。

單一功能

可以針對各別的需求,另外拆分功能,這樣以後在維護、修改上,都會比較方便。

  • 判斷客戶是否存在
  • 發送 email
  • email api
js
const sendEmailApi = (clientData) =>
  request.post('send-email-api-url', clientData)

const getActiveClients = (clients) => clients.filter((client) => client.active)

const sendEmail = (activeClients) => {
  activeClients.forEach((activeClient) => sendEmailApi(activeClient.emailData))
}

OCP 開閉原則

可以任意擴充功能在實體上,但不修改原本功能。

實體對 擴充功能 保持開放,對 修改功能 保持封閉。

假設我們的功能是這樣,取得資料再做後續處理。

js
async function getUserData(newDataType) {
  const { data } = await getUserDataAPI()
  setUserList = data.list
}

如果需要新增判斷,就需要改到原本的代碼,如果之後更多判斷,每次都要一直修改下去....

js
async function getUserData(newDataType) {
  const { status, data } = await getUserDataAPI()
  if (status) return (setUserList = data.list)
  console.log('取得資料失敗')
}

把要執行的事件,抽離出來獨立一個 todo 物件,function 依類型來執行 todo 屬性的事件,之後如果有新增的類型,只需要增加對應的事件在 todo 就可以了,不會修改到 getUserData 功能。

js
const todo = {
  type1: (data) => console.log(data),
  type2: (data) => console.log(data),
  type3: (data) => console.log(data),
  ...
}

async function getUserData(newDataType) {
  const { type, data } = await getUserDataAPI()
  todo[type](data)
}

LSP 里氏替換原則

如果 DogCat 繼承了 Animal (父類別),那 Animal 的所有功能, CatDog 應該都要能夠不發生錯誤

經典錯誤案例

父類 為「矩形」實例,繼承的 子類 為「正方形」實例。

js
class Rectangle {
  constructor(width, height) {
    this.width = width
    this.height = height
  }
  setHeight(newHeight) {
    this.height = newHeight
  }
  getArea() {
    return this.height * this.width
  }
}

class Square extends Rectangle {
  setHeight(newHeight) {
    this.height = newHeight
    this.width = newHeight
  }
}

正方形 調動 矩形 (父類) setHeight 方法時,就產生了錯誤,正方形 的長、寬比已經不是正方形的規則,成為 無效 的正方形。

js
const a = new Rectangle(100, 50)
console.log(a.width, a.height) // 100 50 ✅

const b = new Square(30, 30)
console.log(b.width, b.height) // 30 30 ✅

b.setHeight(50)
console.log(b.width, b.height) // 30 50 ❌

這可以在 Rectangle 使用 多形態 設置 if 來處理正方形的問題或其它方式,但真正的問題為 Square 不是 Rectangle 好的 子類。 實際上 RectangleSquare 都應該繼承自另外的 Shape Class。

ISP 介面隔離原則

不同的介面 有不同的服務,不讓一個 介面 包山包海,裡面會有用不到的功能。 是為了降低維護一個巨大 介面 的成本,每個功能都被拆分為小的部分,只需要針對該部分維護、修改就可以。 降低依賴 「大模組」 !!

直接依賴「大模組」

假設三個 對象 都使用了 OPS 的模組,那 對象 只會使用到其中一個功能,其餘兩個功能都是沒有必要的。

依賴各別介面

大模組對象 之間,設置一個介面,彼此互相隔離、只取用自已需要的功能。

範例

  • 把大模組拆分為更小的接口
  • 將客戶端的依賴轉移到小的接口,這樣客戶端不會直接依整在大的模組。
js
// bankModule.js
export const deposit = () => { ... }
export const withdrawal = () => { ... }
export const transfer = () => { ... }
js
import { deposit } from './bankModule'

DIP 依賴反轉原則

高層模組 -> 抽象介面 -> 底層方法

高層模組 不直接操作 底層方法,是透過 抽象 介面來操作 底層方法 。這樣 底層方法 替換、修改也不影響到 頂層模組

直接依賴低層方法

高層模組 (Example/Example2)downloadFromData 方法,是一個實際執行 fetch 取資料的 底層方法。 當 downloadFromData 修改時,所有依賴的 高層模組 都要同時修改內部的 downloadFromData 方法,這樣對後續維護是一個惡夢。

js
const apiUrl = ''

class Example {
  constructor() {
    ...
  }

  downloadFromData() {
    fetch(apiUrl).then((res) => {
      console.log('取得資料!')
    })
  }
}

class Example2 {
  constructor() {
    ...
  }

  downloadFromData() {
    fetch(apiUrl).then((res) => {
      console.log('取得資料!')
    })
  }
}

透過抽象依賴低層

downloadFromData 內部的底層方法抽離出來在 utils.js, 再依 高層模組 的使用方法各別引入。

提示

Javascript 中,沒有 interface,但可以使用 module 導出 classfunction 來實現這個方式。

utils.js

js
const apiUrl = ''

export const fetchApi = (apiUrl) => {
  return fetch(apiUrl).then((res) => res.json())
}
js
import { fetchApi } from '../utils.js'

class Example {
  constructor() {
    ...
  }

  downloadFromData() {
    fetchApi().then(res => {
      console.log(res)
    })
  }
}

class Example2 {
  constructor() {
    ...
  }

  downloadFromData() {
    fetchApi().then(res => {
      console.log(res)
    })
  }
}

修改底層方法 utils.js

就算修改底層方法,將 fetch 改變為 axios,所有引入的 高層模組 都不用修改,也不會壞掉,而 downloadFromData 不過就是一個 抽象介面 而已。

js
const apiUrl = ''

export const doGet = (apiUrl) => axios.get(apiUrl)

Reference