softwareArchitecture

深入淺出開放封閉原則 Open-Closed Principle

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

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

定義

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

你在講啥 開放到可以擴充 但又要封閉到禁止修改?? 擴充不就是修改嗎

Alt text

其實擴充跟修改他們針對的對象是不同的 但我們最後再回來解釋這句話 因為這句話實在太難懂了

勇於挑戰權威

現在2020年了 身為一個讀書人 我們不能全盤接收外界的知識 即使這是個有名的Principle 我們應該要去理解這個Principle 然後用我們自己的方式去吸收消化

在我看了眾多文獻/線上課程之後 我比較支持另外兩種說法

1.Vasiliy Zukanov 對於OCP的理解

Depend on stable abstractions and modify system’s behavior by providing different realizations

2.Protected Variation: The Importance of Being Closed

Protected Variation pattern: Identify points of predicted variation and create a stable interface around them.

基本上解法很簡單 就是加一層抽象 在OOP的語言中通常指的是介面或是抽象類別

SalaryCalculator

我們繼續來算算員工的薪水吧

class SalaryCalculator{
  public int calculateSalary(Employee employee) {
    //...
    int taxDeduction = calculateTax(employee);
    //...
  }
  private int calculateTax(Employee employee) {
    switch (employee.getType()) {
      case FULL_TIME:
	..
      case CONTRACTOR:
        ..
    }
  }
}

等等 別忘了我們才剛講完SRP 現在 SalaryCalculator 這個類別有超過一個reason to change 違反了SRP 必須把計算稅務的邏輯分開

class SalaryCalculator{
  private TaxCalculator taxCalculator;

  public int calculateSalary(Employee employee) {
    //...
    int taxDeduction = taxCalculator.calculateTax(employee);
    //...
  }
}

class TaxCalculator{
  private int calculateTax(Employee employee) {
    switch (employee.getType()) {
      case FULL_TIME:
        .. 
      case CONTRACTOR:
        ..
    }
  }
}

看起來可愛多了 這樣 SalaryCalculator 就不需要知道 TaxCalculator 是怎麼實作的了

好 SalaryCalculator沒問題 但 TaxCalculator 問題不小 今天要是我要加其他類型的Employee我就必須改我的switch 怎麼辦呢?

沒錯 加一層抽象! 我們讓TaxCalculator變成一個介面

interface TaxCalculator{
  int calculateTax(Employee employee);
}

有了這個抽象之後 原本的兩個switch case各自需要一個類別去實作這個抽象

class TaxCalculatorFullTime implements TaxCalculator{
  int calculateTax(Employee employee){
    // calculate full time tax
  }
}
class TaxCalculatorContractor implements TaxCalculator{
  int calculateTax(Employee employee){
    // calculate contractor tax
  }
}

這…. 看起來有點眼熟啊…

我們好像把演算法給封裝了 而且演算法之間可以彼此互換

我們好像把抽象的方法抽離在interface 把實作留給subclass

這個好像是…

答對了 我們在這裡引用了策略模式

這樣改完之後 SalaryCalculator 會變成怎麼樣呢

class SalaryCalculator{
  private TaxCalculator taxCalculator;

  public int calculateSalary(Employee employee) {
    //...
    TaxCalculator taxCalculator;
    switch (employee.getType()) {
      case FULL_TIME:
        taxCalculator = new TaxCalculatorFullTime();
      case CONTRACTOR:
        taxCalculator = new TaxCalculatorContractor();
    }
    int taxDeduction = taxCalculator.calculateTax(employee);
    // ...
  }
}

老話一句 你過來我保證不打死你

Alt text

你只是把switch從TaxCalculator移到SalaryCalculator而已啊 那如果今天我要加個不同種類的員工 我就要動到SalaryCalculator了 說好的SRP呢

別著急 你仔細看一下這個switch的長相

Alt text

你可以再靠近一點

Alt text

你可以再靠近一點點

Alt text

看完之後告訴我這是什麼 沒錯 工廠 簡單工廠

class TaxCalculatorFactory{
  public TaxCalculator taxCalculatorFactory.newTaxCalculator(EmployeeType employeeType) {
    switch (employeeType) {
      case FULL_TIME:
        return new TaxCalculatorFullTime();
      case CONTRACTOR:
        return new TaxCalculatorContractor();
      default:
	return new TaxCalculatorDefault();
    }
  }
}

有了這個工廠之後 SalaryCalculator變成這樣

class SalaryCalculator{
  private TaxCalculatorFactory taxCalculatorFactory;

  public int calculateSalary(Employee employee) {
    //...
    TaxCalculator taxCalculator = taxCalculatorFactory.newTaxCalculator(employee.getType());
    int taxDeduction = taxCalculator.calculateTax(employee);
    //...
  }
}

實在是太簡潔啦

來個架構圖吧

黑箭頭代表依賴 白箭頭代表實作

Alt text

精美 今天如果有一個新的計算稅率方式 有什麼地方要改呢

1.當然 要有個新的TaxCalculatorX類別

2.TaxCalculatorFactory要知道何時需要生成這個類別

Alt text

搞定 支持新的TaxCalculator類別就是TaxCalculatorFactory的reason to change

SalaryCalculator完全沒動 TaxCalculator完全沒動

看回OCP

回到我們本章的主題 什麼是OCP呢

先來看Vasiliy Zukanov的版本

Depend on stable abstractions and modify system’s behavior by providing different realizations

TaxCalculator 就是這個stable abstractions 而TaxCalculatorFullTime跟TaxCalculatorContrator就是different realizations

再來看Craig Larman的版本

Protected Variation pattern: Identify points of predicted variation and create a stable interface around them.

predicted variation就是稅金計算的方式 而stable interface就是TaxCalculator

這兩個解釋都已經講得很清楚了 但為了給最元老的OCP尊重 我們還是勉為其難來看一下

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

這下你終於懂了 close到底是close什麼 就是要有一個不會改變的抽象 open到底是open什麼 就是要能夠提供彈性的實作

所以本文第一段所說 擴充跟修改他們針對的對象是不同的 是這個意思

初初學者友善教學

我想 對於Uncle Bob這種等級的大神 這種概念他們早就已經內化成他們日常生活的一部分 只是為了出書或是教學 把腦中的概念模糊的寫出來 殊不知 初學者根本看不懂你想表達的是什麼

當然我也期許未來的某一天我也可以到達大神境界 但在那之前 我先把我的讀書心得寫下來 讓初學者的心得可以對初初學者的學習有幫助 那就不枉費這篇文章

總結

這篇文章告訴你為什麼一個恰當的抽象 可以讓你的架構多出許多的彈性

那為什麼這篇文章還提到了工廠模式跟策略模式呢

再重申一次 SOLID 只是一個心法 而工廠模式跟策略模式這兩個是我們用來遵循OCP的手段

就像你想要殺死被生死蟲感染的殭屍 我跟你說他只要頭沒了就掛了(心法) 那你是要用刀砍斷(手段1) 還是用火槍爆頭(手段2)隨你高興

延伸閱讀

業務複雜=if else?剛來的大神竟然用策略+工廠徹底幹掉了他們!