iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Software Development

幫自己搞懂物件導向和設計模式系列 第 12

開放封閉原則 Open-Closed Principle

在上一篇文章當中,我們提到「單一功能原則」,指每一個類別只會因為一種原因被修改。那麼,如果真的遇到需求變動、需要修改的時候,我們該如何「修改」呢?

SOLID 當中的開放封閉原則 Open-Closed Principle 提供了修改的原則:

In object-oriented programming, the open–closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"; that is, such an entity can allow its behaviour to be extended without modifying its source code.

在物件導向程式設計當中,開放封閉原則指的是,在軟體當中的實體 (不管是 classes, modules, or functions) 應該對於「擴展」是開放的,但對於「修改」是封閉的

好吧,剛剛說到要「修改」,但是這裡又說要對「修改」封閉,到底是什麼意思呢?

先讓我們來看看下面的例子吧

計算面積

假設今天我們成立一間公司,專門負責計算形狀面積,像是

const calculateArea = (object) => {
  return xxx
}

當第一個需求上門的時,我們希望可以處理長方形的面積。因此我們建立了長方形的類別

class Rectangle {
  width: number
  length: number

  constructor(width: number, length: number) {
    this.width = width
    this.length = length
  }
}

接著,更新 calculateArea 方法:

const calculateArea = (object) => {
  return object.width * object.length
}

最後成功計算出面積

const rectangle = new Rectangle(13, 17)
calculateArea(rectangle)                  // 221

當需求變動時

當公司業務蒸蒸日上,突然有個新客戶出現,希望我們也能夠計算三角形的面積,這時候我們該怎麼辦呢?

好吧,那麼我們就讓 calculateArea 方法可以根據不同需求來計算面積。因此首先先調整原本的 Rectangle 類別,以及建立新的 Triangle 類別

class Rectangle {
  shape: string
  width: number
  length: number

  constructor(width: number, length: number) {
    this.width = width
    this.length = length
    this.shape = 'rectangle'
  }
}


class Triangle {
  shape: string
  base: number
  height: number

  constructor(base: number, height: number) {
    this.base = base
    this.height = height
    this.shape = 'triangle'
  }
}

接著讓我們更新 calculateArea,讓他能夠根據不同的形狀,回傳正確的面積(如果不是認識的形狀,回傳 null)

const calculateArea = (object) => {
  switch (object.shape) {
    case 'rectangle':
      return object.width * object.length
    case 'triangle':
      return object.base * object.height / 2
    default:
      return null  
  }
}

之後就能夠順利得到結果

const a = new Rectangle(13, 17)
const b = new Triangle(11, 19)
calculateArea(a)                // 221
calculateArea(b)                // 104.5

當需求持續變動時

後來公司生意越做越大,也開始有更多以前沒有看過的需求進來。每一次有新的需求(形狀)出現,我們就要去修改 calculateArea 方法。另外,在增加新的計算方法的時候,很有可能就不小心碰到原本的方法,進而造成錯誤發生。

這時候有沒有什麼方法可以讓事情變得簡單一點呢?如果我們不要自己為各種形狀建立計算面積的方法,而是讓他們(形狀)自己來告訴我們呢?

讓我們來重新整理一下程式碼。首先,建立一個叫做 Shape 的介面,他要求所有執行這個介面的類別,都需要有一個 getArea 的方法

interface Shape {
  getArea(): number
}

接著,讓原本的 RectangleTriangle 都執行 Shape 介面,並定義好 getArea ,也就是計算面積的方法

class Rectangle implements Shape {
  width: number
  length: number

  constructor(width: number, length: number) {
    this.width = width
    this.length = length
  }

  getArea() {
    return this.width * this.length
  }
}


class Triangle implements Shape {
  base: number
  height: number

  constructor(base: number, height: number) {
    this.base = base
    this.height = height
  }

   getArea() {
    return this.base * this.height / 2
  }
}

接著,calculateArea 方法可以簡化成

const calculateArea = (object) => {
  return object.getArea()
}

最後可以得到同樣的結果

const a = new Rectangle(13, 17)
const b = new Triangle(11, 19)
calculateArea(a)                // 221
calculateArea(b)                // 104.5

在這樣的情況下,不管未來有什麼奇怪的形狀出現,我們只要建立新的類別,執行 Shape 介面,不需要更動 calculateArea 方法了!

開放和封閉

從上面的例子來看,我們在做的事情就是,當新類別出現的時候

  • 「開放」類別(業務)的擴展,只要他們有執行 Shape 介面
  • 「封閉」對 calculateArea 的修改

雖然這裡的 calculateArea 只是一個獨立的方法,但他也可以是某個類別當中的一個方法。也就是說,當需求變動的時候,我們不需要進入這個類別來修改方法。

小結

如果能實現「開放封閉原則」,就代表程式碼具備擴展性和穩定性,不會因為需求而持續修改程式碼,也讓程式碼更容易維護。

當然在真實世界複雜的情況下,可能無法做到純粹的不做任何修改,但如果能夠盡可能降低修改的發生,那麼就能夠降低錯誤發生的可能性。


上一篇
單一功能原則 Single Responsibility Principle
下一篇
依賴反轉原則 Dependency Inversion Principle
系列文
幫自己搞懂物件導向和設計模式30

尚未有邦友留言

立即登入留言