softwareArchitecture

深入淺出單一職責原則 Single Responsibility Principle

這篇文章介紹軟體架構裡面 S.O.L.I.D 中的S (Single Responsibility Principle)

這篇文章中大部分的程式碼 參考自SOLID Principles of Object-Oriented Design and Architecture

定義

Each class in your system should have only one responsibility

講完了各位 收工

Alt text

就這樣啊 一個類別就是做一件事 超過一件事就要把這個類別拆成其他小類別 有什麼難的??

Uncle Bob說 SRP最容易讓人誤解的部分就是他的名稱本身 因為大家誤會了responsibility這個字的意思

我們來看個例子吧

class AuthManager {
  public void logIn(String userName, String password) {
    //...
  }
  public void logOut() {
    //...
  }
}

你會說 這個類別違反了SRP 因為他做了兩件事 登入跟登出

但我也可以說這個類別負責Handle user’s authentication 所以他只做一件事

Alt text

那這樣怎麼搞呢 我也不能說你錯 你也不能說我錯啊

所以 我們需要重新定義SRP

A module should have one, and only one, reason to change

恩 這樣就比較直接 不會有吵架的問題 我們來看Clean Architecture裡的例子

Employee

class Employee {
  public int calculateMonthlySalary() {
    //...
  }
  public HoursReport produceMonthlyHoursReport() {
    //...
  } 
  public void saveEmployee() {
    //...
  }
}

今天我們有個可愛的Employee 裡面有三個功能 計算每月薪水 產生每月工時報告 儲存Employee資料

那我們來看看這個類別有哪些”reasons to change”呢??

1.如果會計部想改變時薪 calculateMonthlySalary() 要改

2.如果人資想改變加班計算方式 produceMonthlyHoursReport() 要改

3.如果工程師想改變Employee的encode方式 saveEmployee() 要改

合理 這個類別違反了SRP 那到底違反SRP會怎樣呢?

慘劇

你們公司的資深工程師剛走 你是個剛上工不久的新手碼農 今天人資跟你說 Hey 政府頒布政令 原本工作超過8小時算加班 現在要工作超過10小時才算加班 請幫我改變一下HoursReport的產生方式 讓我們可以清楚地看到誰加班太久

你說這還不簡單 我這就改變produceMonthlyHoursReport()的實作 改完後推上production HR很滿意覺得你是天才 過了兩個禮拜發薪日你才覺得奇怪 為什麼你拿的錢變多了呢 然後公司CFO打給你 說你們公司虧了好幾個M 你驚覺慘了 再仔細看你的程式 發現

Alt text

剉賽了 calculateMonthlySalary() 居然在呼叫produceMonthlyHoursReport() HR部門跟你要的新功能影響了會計部的舊功能 好了你可以安心等著被火了

慘劇講完了 這雖然是刻意編造出來的例子 但在現實生活中很常出現 因為函式跟函式之間過度耦合(coupled) 所以常常一個小改動會出現很多副作用(side effect)

SRP violations lead to excessive coupling

要是當初資深工程師有遵守SRP的話 你今天就不會犯下這個錯誤了 但事已至此 安心上路

該怎麼改

把未來很可能會改變的函式分離到其他的類別

class Employee {
  private String id;
  public String getId(){
   return id;
  }
}
class PaymentService{
  public int calculateMonthlySalary(Employee employee) {
  //...
  }
}
class WorkHoursServiceService{
  public HoursReport produceMonthlyHoursReport(Employee employee) {
  //...
  }
}
class EmployeeDAO{
  public void saveEmployee(Employee employee) {
  //...
  }
}

就是這樣 這樣你會發現 每個類別都只有一個reason to change 那你就可以放心地說 每個類別都遵守SRP

AuthManager

我們來看回一開始的問題

class AuthManager {
  public void logIn(String userName, String password) {
    //...
  }
  public void logOut() {
    //...
  }
}

這個類別有沒有符合SRP呢 給你1分鐘想想

 

 

 

 

 

答案是 It depends

Alt text

冷靜冷靜 一個類別有沒有符合SRP 是決定於實作

來看這個很廢的實作

class AuthManager {
  private String loggedInUserName = "";

  public void logIn(String userName, String password) {
    if(userName.compareTo("jyt0532") == 0 && password.compareTo("1234") == 0) {
      loggedInUserName = userName;
    }
    if(userName.compareTo("boyu") == 0 && password.compareTo("1234") == 0) {
      loggedInUserName = userName;
    }
  }
  public void logOut() {
    loggedInUserName = "";
  }
}

醜話先說在前頭 千萬不要這麼寫 但如果你真的這麼寫了 這個類別有什麼改變的理由呢??

1.改變密碼

2.Add/Remove 可以login的人

但仔細想想 這兩個其實是同一件事 只要你驗證的方式想改變 你就必須改變這個類別 所以這個實作是符合SRP的

好那我們再來看下一個實作

class AuthManager {
  private String loggedInUserName = "";

  public void logIn(String userName, String password) {
    String hash = hashPassword(password);
    if(checkMatchInDB(userName, hash)) {
      loggedInUserName = userName;
    }
  }
  public void logOut() {
    loggedInUserName = "";
  }
  public String hashPassword(String password) {
    // hash algorithm
  }
  public boolean checkMatchInDB(String userName, String hash) {
    // call DB
  }
}

這個類別有什麼改變的理由呢?

1.想改變hash的演算法的時候

2.想改變底層DB的時候

所以 第二個實作 違反了SRP

那該怎麼改AuthManager

把你剛剛想到的reason to change各自分離成其他的類別

class AuthManager {
  private String loggedInUserName = "";
  private PasswordHasher passwordHasher;
  private AuthChecker authChecker;

  public void logIn(String userName, String password) {
    String hash = passwordHasher.hashPassword(password);
    if(authChecker.checkMatchInDB(userName, hash)) {
      loggedInUserName = userName;
    }
  }
  public void logOut() {
    loggedInUserName = "";
  }
}

這樣看起來 AuthManager 就有遵循SRP

打破砂鍋

等等等等 你這樣看起來對 但我可不可以這樣說呢 AuthManager有兩個理由改變

1.logIn流程改變(比如說我想寄email)

2.logOut流程改變(比如我想寄email)

這樣子的話是不是我也可以說AuthManager沒遵守SRP呢

好問題 有一個詞叫做over-engineering 就是你把事情想得太複雜 我並不是要把我回答不出來的問題都歸為over-engineering 只是如果你要這樣說 那所有的類別不就都只能有一個public的方法嗎 因為真的硬要講 你每個方法都有可能改啊

所以我的心得是 SOLID 只是一個心法 是你設計一個系統的時候 必須了解並且銘記於心的東西 所以當你設計AuthManager的時候 你必須要知道哪些是未來很可能會發生的改變 哪些在未來幾乎不會改變

今天我知道logIn流程很可能會改變 這就是我AuthManager的reason to change

今天我知道logOut大概不太會變了 我就不理它 要是下個月PM真的想要改變logOut流程 那就到時候再把AuthManager拆成loginManager跟logOutManager就可以

所以需要細分到什麼程度 取決於你對於你商業邏輯的了解 沒有一定的標準答案 我們的設計目標 是在架構整潔的情況下達成最大的彈性