SOLID 單一職責原則

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

-8 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 (台北標準時間)