設計原則 - 開放封閉原則 Open-Closed Principle

在軟體設計中,我們經常面對需求變更的挑戰。想像一下,當客戶突然提出新需求時,你的系統卻因為架構過於死板,導致每次要修改功能都得重新調整大量程式碼。這不僅會耗費時間,還會增加出錯的機會。這時候「開放封閉原則 (Open-Closed Principle, OCP)」就能派上用場,幫助我們的程式保持靈活性,同時又能控制變更帶來的風險。

什麼是開放封閉原則?

開放封閉原則,簡單來說就是軟體應該對擴展開放,對修改封閉。也就是說我們應該設計程式,使其能夠在不修改既有程式碼的情況下,透過擴展來新增功能。

換句話說,當我們的需求改變時,應該可以透過增加新類別或新功能來解決,而不是去修改現有的邏輯。這樣的好處是,既能保護原有系統的穩定性,又能快速適應變化。

開放封閉原則的例子

讓我們來看一個不符合開放封閉原則的例子,來感受這個原則的意義。

錯誤的設計,違反開放封閉原則

假設我們在設計一個圖形繪製程式,原本只需要支援兩種圖形——圓形和方形,我們的程式可能會這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum ShapeType {
Circle,
Square
};

class ShapeDrawer {
public:
void drawShape(ShapeType type) {
if (type == Circle) {
// 畫圓形的邏輯
} else if (type == Square) {
// 畫方形的邏輯
}
}
};

這段程式碼運作良好,但當需求改變,客戶希望支援更多的圖形類型時,我們就必須不斷修改 ShapeDrawer 類別,加入新的條件分支。如果圖形種類越來越多,例如三角形、橢圓與菱形等,這段程式碼會變得越來越臃腫且難以維護。

上述例子顯然違反了開放封閉原則。因為每當我們需要新增一種新圖形,都得進行程式碼修改,這不僅增加了錯誤的風險,也讓系統變得更加脆弱。理想的情況下,我們應該能在不改動既有程式碼的情況下,輕鬆地增加新圖形的繪製功能。

正確的設計,遵守開放封閉原則

為了符合開放封閉原則,我們可以運用抽象類別與多型的設計來解決這個問題:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Shape {
public:
virtual void draw() const = 0;
};

class Circle : public Shape {
public:
void draw() const override {
// 畫圓形的邏輯
}
};

class Square : public Shape {
public:
void draw() const override {
// 畫方形的邏輯
}
};

class ShapeDrawer {
public:
void drawShape(const Shape& shape) {
shape.draw();
}
};

在這個設計中,ShapeDrawer 不再依賴於具體的圖形類型,所有圖形的繪製邏輯都由各自的類別來負責。如果我們需要新增一種新圖形,例如三角形,我們只需新增一個 Triangle 類別,實作其 draw 方法,而不需要修改 ShapeDrawer 類別。這樣我們就可以輕鬆擴展功能,而不必改動既有的程式碼,符合了開放封閉原則。

為什麼這麼做?

開放封閉原則帶來的最大好處就是系統的穩定性與可擴展性。當我們能夠在不修改現有程式碼的情況下擴展功能時,就能減少意外錯誤的風險,也能保持系統的穩定性。這在大型專案或持續演進的系統中顯得重要。

同時,這樣的設計還讓我們的程式更加容易維護和理解。每個圖形的繪製邏輯都被封裝在各自的類別中,遵循單一職責原則 (Single Responsibility Principle),不會讓 ShapeDrawer 類別變得臃腫不堪。

當然,開放封閉原則也有其限制,特別是當我們過度抽象時,可能會使系統設計變得過於複雜。在小型專案中,這種設計的好處可能不那麼明顯,但隨著系統規模增大,這種方式的優勢會逐漸顯現。

實際應用

開放封閉原則廣泛應用於各種場景,尤其是在框架設計第三方擴展時非常實用。舉例來說,許多應用程式框架都提供了一系列的抽象介面,開發者可以在不修改框架核心程式碼的情況下,透過實作這些介面來擴展功能,這正是開放封閉原則的應用。

同樣地,在一些策略模式或工廠模式的應用中,我們也會看到開放封閉原則的影子。無論是新增業務邏輯、加入新的功能模組,還是替換現有功能,遵守這個原則都能讓我們的系統保持穩定且靈活。