SOLID 單一職責原則

SOLID 單一職責原則(SRP, Single Responsibility Principle)

· 2 min read

每一個類別或模組應該只有一個職責,且應該只有一個引起修改它的原因。

例如:一個類別負責處理訂單,那麼他就應該只負責處理訂單相關的事物,而不是混雜其他的功能。

SRP 的目的是提高程式碼的可讀性和可維護性,因為每個類別只負責單一職責,使得程式碼更容易理解和修改。

生活中的範例

假設你是一個中學生,你參加了學校的籃球隊。在這支籃球隊中,每個人都有自己的職責,例如得分手、防守專家、籃板手等等。如果每個人都想嘗試做所有的事情,例如每個人都想得分、防守和搶籃板,那麼這支籃球隊就可能會出現問題。因為每個人都想做所有的事情,沒有人專注在自己的職責上,這可能會導致比賽失利。當發生失分的情況,教練想要針對職責去調整行為,可能會導致需要跟每個人溝通。

相反地,如果每個人都專注在自己的職責上,例如得分手專注於得分、防守專家專注於防守、籃板手專注於搶籃板,那麼這支籃球隊就可能會更加成功。每個人都有自己的職責和專業領域,這樣可以更好地發揮團隊的合作和協作能力。當發生失分的情況,教練想要針對職責去調整行為,例如我方搶到籃板球的次數很少,只需要追究專門搶籃板的中鋒就好。

這就是單一職責原則在籃球比賽中的應用,讓每個人專注於自己的職責上,這樣可以讓整個團隊更加成功。同樣地,在生活中,如果我們也能遵循單一職責原則,將不同的職責分開,那麼我們也可以更加高效地完成各種任務。

JavaScript 類物件導向範例

錯誤範例

在這個例子中,Calculator 類別不僅負責四則運算,還包括一個 calculate 方法,該方法將複雜的算術表達式作為參數,並返回計算結果。這違反了單一職責原則,因為這個類別超出了其職責範圍,應該由其他類別來負責該功能。

class Calculator {
  add(a, b) {
    const result = a + b
    console.log(`The sum of ${a} and ${b} is ${result}.`)
    return result
  }

  subtract(a, b) {
    const result = a - b
    console.log(`The difference between ${a} and ${b} is ${result}.`)
    return result
  }

  multiply(a, b) {
    const result = a * b
    console.log(`The product of ${a} and ${b} is ${result}.`)
    return result
  }

  divide(a, b) {
    if (b === 0)
      throw new Error('Cannot divide by zero')

    const result = a / b
    console.log(`The quotient of ${a} and ${b} is ${result}.`)
    return result
  }

  calculate(expression) {
    // 在這個計算機類別中,新增了一個計算複雜表達式的方法,這超出了該類別的責任範圍
    // 這種行為應該由其他類別來負責
    // 這違反了單一職責原則
    const parts = expression.split(' ')
    const a = parseFloat(parts[0])
    const operator = parts[1]
    const b = parseFloat(parts[2])

    switch (operator) {
      case '+':
        return this.add(a, b)
      case '-':
        return this.subtract(a, b)
      case '*':
        return this.multiply(a, b)
      case '/':
        return this.divide(a, b)
      default:
        throw new Error(`Unsupported operator: ${operator}`)
    }
  }
}

正確範例

在這個範例中,將計算機 UI 相關的工作從計算機類別中獨立出來,將其封裝在 CalculatorUI 類別中,符合單一職責原則。

class Calculator {
  add(a, b) {
    return a + b
  }

  subtract(a, b) {
    return a - b
  }

  multiply(a, b) {
    return a * b
  }

  divide(a, b) {
    if (b === 0)
      throw new Error('Cannot divide by zero')

    return a / b
  }
}

class CalculatorUI {
  constructor(calculator) {
    this.calculator = calculator
  }

  // 顯示輸入介面
  showInput() {}

  // 顯示輸出結果
  showOutput(result) {}

  // 綁定按鈕事件
  bindButtonEvents() {}

  // 取得輸入資料
  getInputData() {}

  // 計算並顯示結果
  calculate() {
    const { num1, num2 } = this.getInputData()
    const result = this.calculator.add(num1, num2)
    this.showOutput(result)
  }
}

// 計算機類別只負責進行四則運算,而 CalculatorUI 負責顯示輸入介面、顯示輸出結果、綁定按鈕事件、取得輸入資料和計算並顯示結果等 UI 相關的工作。

JavaScript FP 範例

錯誤範例

假設我要實現一個取得上個月同一天的日期的函式,可以這樣實現

function getLastDayOfLastMonth(value) {
  // 為了怕傳入的 value 不是 Date 物件,所以寫了一段防呆
  if (!(value instanceof Date && value.toString() !== 'Invalid Date'))
    return value

  // 開始處理取得上個月份的日期
  const previousMonth = new Date(value.getTime())
  previousMonth.setDate(0)

  return previousMonth
}

const date = new Date(2022, 0, 12)
console.log(date)
// Wed Jan 12 2022 00:00:00 GMT+0800 (台北標準時間)

const lastDayOfLastMonth = getLastDayOfLastMonth(date)
console.log(lastDayOfLastMonth)
// Fri Dec 31 2021 00:00:00 GMT+0800 (台北標準時間)

正確範例

可以看到這個函式除了要實作取得上個月日期之外,還需要去判斷「某值是否為合法的日期物件」,這並不符合單一職責原則,所以我們可以把判斷合法日期物件的邏輯抽出,分離職責,實作 isValidDateObject。

function isValidDateObject(value) {
  return value instanceof Date && value.toString() !== 'Invalid Date'
}

function getLastDayOfLastMonth(value) {
  // 為了怕傳入的 value 不是 Date 物件,所以寫了一段防呆
  if (!isValidDateObject(value))
    return value

  // 開始處理取得上個月份的日期
  const previousMonth = new Date(value.getTime())
  previousMonth.setDate(0)

  return previousMonth
}

const date = new Date(2022, 0, 12)
console.log(date)
// Wed Jan 12 2022 00:00:00 GMT+0800 (台北標準時間)

const lastDayOfLastMonth = getLastDayOfLastMonth(date)
console.log(lastDayOfLastMonth)
// Fri Dec 31 2021 00:00:00 GMT+0800 (台北標準時間)