SOLID 開放封閉原則

SOLID 開放封閉原則(OCP, Open-Closed Principle)

-5 min read

程式碼的設計應該是「開放擴展」(Open for extension),同時「封閉修改」(Closed for modification),換句話說,當需求改變的時候,程式碼的設計應該可以輕鬆擴展,而不必修改原有的程式碼。

例如,當新增一個新功能時,我們可以透過新增新的程式碼,而不是直接修改現有的程式碼來實現。這樣可以降低對原有程式碼的風險,因為修改可能會導致意外的錯誤。同時,透過擴展現有的程式碼,可以避免重複程式碼,從而提高程式碼的可重用性。

生活中的範例

對於更換刮刀的頭,我們不需要去修改內部構造,只需要換刀頭然後啟動電源就好。

如果對於刮刀的頭有不同的需求,只需要符合刮刀的「介面」,讓刮刀頭能接上刮刀,重新開發一個頭就好了,不需要整個刮刀都修改。

  • 對於擴展是開放的 — 當需求變更時模組行為可以新增的
  • 對於修改是封閉的 — 當進行擴展時,不需修改既有的程式碼

JavaScript 類物件導向範例

錯誤範例

假設我們現在有一個 Shape 類別,它有一個 calculateArea 方法,用於計算圓形(circle)和正方形(square)的面積。現在我們想要增加一個新的圖形,例如矩形(rectangle),並且也想讓 Shape 類別能夠計算矩形的面積。如果我們直接在 Shape 類別中添加新的計算矩形面積的程式碼,這就違反了開放封閉原則,因為我們修改了現有的程式碼。在這個例子中,當我們需要添加新的圖形時,必須修改 Shape 類別的 calculateArea 方法。但是,這會導致類別的行為發生變化,因為它不再只計算圓形和正方形的面積,而是增加了計算矩形面積的功能。

class Shape {  constructor(type, width, height, radius) {    this.type = type    this.width = width    this.height = height    this.radius = radius  }  calculateArea() {    if (this.type === 'circle') {      return Math.PI * this.radius ** 2    }    else if (this.type === 'square') {      return this.width ** 2    }    else if (this.type === 'rectangle') { // 添加新的計算矩形面積的程式碼,違反了開放封閉原則      return this.width * this.height    }  }}

正確範例

如果我們希望符合開放封閉原則,我們可以將 Shape 類別設計成抽象類別,並定義一個 calculateArea 方法的抽象方法,讓子類別實現這個方法來計算不同圖形的面積。下面是一個修改後符合開放封閉原則的範例:

class Shape {  constructor(type, width, height, radius) {    this.type = type    this.width = width    this.height = height    this.radius = radius  }  calculateArea() {    throw new Error('calculateArea method must be implemented')  }}class Circle extends Shape {  constructor(radius) {    super('circle', null, null, radius)  }  calculateArea() {    return Math.PI * this.radius ** 2  }}class Square extends Shape {  constructor(width) {    super('square', width, null, null)  }  calculateArea() {    return this.width ** 2  }}class Rectangle extends Shape {  constructor(width, height) {    super('rectangle', width, height, null)  }  calculateArea() {    return this.width * this.height  }}