2026-05-19 C# OCP 的應用情境式舉例

2026-05-19 C# OCP 的應用情境式舉例

我:
OCP 的應用:應盡量避免修改已有的類別,而是通過繼承和覆寫來擴展功能。這樣可以減少對已有代碼的改動,降低引入錯誤的風險。
這個不太懂, 可以情境式舉例嗎? 最好能利用日常生活情境

GPT:
當然可以,讓我用一個日常生活中的例子來解釋開放-封閉原則(Open/Closed Principle, OCP),並展示如何應用它來擴展功能而非修改原有的類別

日常生活情境 - 咖啡機設計

想像你是一名設計師,正在為一個咖啡館設計一個咖啡機系統。這台咖啡機最初的功能非常簡單,只能製作美式咖啡(Americano)

第一版設計 - 單一功能

你設計了一個類別 CoffeeMachine,其中有一個方法 MakeCoffee() 專門用於製作美式咖啡。

1
2
3
4
5
6
7
public class CoffeeMachine
{
public void MakeCoffee()
{
Console.WriteLine("Making an Americano...");
}
}

這樣的設計可以滿足當前的需求,只需呼叫 MakeCoffee() 方法,咖啡機就會製作美式咖啡。

新需求 - 加入新咖啡種類

隨著時間的推移,咖啡館的客人越來越多,他們開始希望能夠製作其他種類的咖啡,例如拿鐵(Latte)卡布奇諾(Cappuccino)。此時你面臨的挑戰是如何在不改動原有 CoffeeMachine 類別的情況下,滿足這些新需求。

為什麼不直接改動原有類別?

如果你直接在 CoffeeMachine 中新增更多的咖啡製作方法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CoffeeMachine
{
public void MakeCoffee()
{
Console.WriteLine("Making an Americano...");
}

// 新增的功能
public void MakeLatte()
{
Console.WriteLine("Making a Latte...");
}

public void MakeCappuccino()
{
Console.WriteLine("Making a Cappuccino...");
}
}

這樣的做法有幾個潛在的問題:

  1. 增加了代碼的複雜性:隨著咖啡種類的增加,類別中的代碼會越來越複雜。
  2. 違反開放-封閉原則:每次需要加入新咖啡種類時,都需要修改原有類別,這會增加引入錯誤的風險,並且在已有代碼的基礎上進行改動不容易維護。

遵循 OCP 的設計 - 擴展而不修改

為了遵循 OCP,應對現有的類別保持不變,並通過擴展來添加新功能。這裡可以使用繼承覆寫來擴展咖啡機的功能。

首先,我們可以將 CoffeeMachine 類別設計為一個基類,然後讓不同種類的咖啡機繼承自這個基類並實現特定的功能。

基類設計

1
2
3
4
5
6
7
public class CoffeeMachine
{
public virtual void MakeCoffee()
{
Console.WriteLine("Making an Americano...");
}
}

這樣,CoffeeMachine 類別仍然只具備製作美式咖啡的功能,並且設計為可擴展的,方法 MakeCoffee() 使用了 virtual 關鍵字,表示它可以被子類覆寫。

擴展功能 - 製作不同種類的咖啡

接下來,我們通過繼承 CoffeeMachine 類別來擴展咖啡機的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LatteMachine : CoffeeMachine
{
public override void MakeCoffee()
{
Console.WriteLine("Making a Latte...");
}
}

public class CappuccinoMachine : CoffeeMachine
{
public override void MakeCoffee()
{
Console.WriteLine("Making a Cappuccino...");
}
}
  • LatteMachineCappuccinoMachine 都繼承自 CoffeeMachine,並且覆寫了 MakeCoffee() 方法來製作不同種類的咖啡。
  • 這樣,我們在不改動原有的 CoffeeMachine 類別的情況下,通過擴展新增了拿鐵和卡布奇諾的製作功能。

使用擴展後的類別

當我們想製作不同種類的咖啡時,只需使用相應的子類來創建咖啡機物件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Program
{
static void Main(string[] args)
{
CoffeeMachine myAmericano = new CoffeeMachine();
myAmericano.MakeCoffee(); // 輸出: Making an Americano...

CoffeeMachine myLatte = new LatteMachine();
myLatte.MakeCoffee(); // 輸出: Making a Latte...

CoffeeMachine myCappuccino = new CappuccinoMachine();
myCappuccino.MakeCoffee(); // 輸出: Making a Cappuccino...
}
}

遵循 OCP 的優點

  1. 擴展性強

    • 現在,如果咖啡館再增加新種類的咖啡,例如摩卡(Mocha),只需要創建一個新的子類 MochaMachine 並覆寫 MakeCoffee() 方法。
    • 不需要改動原有的 CoffeeMachine 類別,這樣保持了原有代碼的穩定性。
  2. 降低錯誤風險

    • 原有的 CoffeeMachine 類別保持不變,不會受到新需求的影響。因此,如果原本的美式咖啡功能運行良好,新需求的加入不會改變它的行為,降低了引入錯誤的風險。
  3. 符合開放-封閉原則

    • 類別對擴展是開放的(可以通過繼承來擴展功能),對修改是封閉的(不需要直接修改原有的類別)。
    • 這樣的設計使得系統更具靈活性,當需要擴展新功能時,只需新增子類即可,降低了修改已經穩定的代碼的風險。

總結

  • 開放-封閉原則(OCP) 強調類別應該對擴展開放(允許增加新功能)而對修改封閉(不必改動已有代碼)。
  • 在我們的咖啡機的例子中,通過繼承和覆寫來實現新功能的擴展,而不是改動原有的類別。
  • 這樣的做法能夠減少代碼改動,保持原有的代碼穩定,降低錯誤風險,同時也使系統更加靈活,能夠快速應對變化和需求的增長。

希望這個日常生活中的例子能幫助你更好地理解 OCP 的應用和它的重要性。如果你有其他問題或需要進一步的說明,隨時告訴我!

我:
那麼以上面的例子是否有最安全且降低耦合的寫法呢?

是的,在面向對象編程中,遵循 開放-封閉原則(OCP) 的同時,還可以進一步降低類別之間的耦合性,這樣能讓系統更具彈性可測試性,並且更加符合設計的最佳實踐。為了達到這些目標,我們可以利用一些設計模式和設計原則,比如依賴注入(Dependency Injection)策略模式(Strategy Pattern)。以下是一些改進的方法,可以使得系統更安全並降低耦合。

1. 依賴注入(Dependency Injection)

為了降低 CoffeeMachine 與特定咖啡類型的緊密耦合,我們可以使用 依賴注入 來管理不同的咖啡製作行為。依賴注入使得 CoffeeMachine 不再直接依賴某個特定的類別,而是依賴於一個抽象的接口,這樣能提高系統的靈活性。

改進的設計 - 抽象接口和依賴注入

首先,定義一個接口 ICoffeeMaker,該接口包含 MakeCoffee() 方法,代表一種製作咖啡的行為。

1
2
3
4
public interface ICoffeeMaker
{
void MakeCoffee();
}

接著,我們為不同的咖啡類型(例如美式、拿鐵、卡布奇諾)分別實現這個接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AmericanoMaker : ICoffeeMaker
{
public void MakeCoffee()
{
Console.WriteLine("Making an Americano...");
}
}

public class LatteMaker : ICoffeeMaker
{
public void MakeCoffee()
{
Console.WriteLine("Making a Latte...");
}
}

public class CappuccinoMaker : ICoffeeMaker
{
public void MakeCoffee()
{
Console.WriteLine("Making a Cappuccino...");
}
}

接下來,我們修改 CoffeeMachine 類別,使它依賴於 ICoffeeMaker 接口,而不是依賴於具體的咖啡製作方式。通過依賴注入的方式,我們可以動態地改變咖啡機的行為。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CoffeeMachine
{
private ICoffeeMaker _coffeeMaker;

// 依賴注入,用於注入不同的咖啡製作方式
public CoffeeMachine(ICoffeeMaker coffeeMaker)
{
_coffeeMaker = coffeeMaker;
}

public void MakeCoffee()
{
_coffeeMaker.MakeCoffee();
}
}

這樣的設計可以讓 CoffeeMachine 非常靈活,我們可以根據需要注入不同的 ICoffeeMaker 實現,而不需要修改 CoffeeMachine 本身。

使用範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Program
{
static void Main(string[] args)
{
// 使用不同的咖啡製作器來創建咖啡機
ICoffeeMaker americanoMaker = new AmericanoMaker();
CoffeeMachine americanoMachine = new CoffeeMachine(americanoMaker);
americanoMachine.MakeCoffee(); // 輸出: Making an Americano...

ICoffeeMaker latteMaker = new LatteMaker();
CoffeeMachine latteMachine = new CoffeeMachine(latteMaker);
latteMachine.MakeCoffee(); // 輸出: Making a Latte...

ICoffeeMaker cappuccinoMaker = new CappuccinoMaker();
CoffeeMachine cappuccinoMachine = new CoffeeMachine(cappuccinoMaker);
cappuccinoMachine.MakeCoffee(); // 輸出: Making a Cappuccino...
}
}

2. 策略模式(Strategy Pattern)

這樣的設計其實是一種 策略模式(Strategy Pattern) 的應用。策略模式是一種設計模式,通過將一個行為抽象為一個接口或基類,使得可以在運行時更換具體的行為實現,而不需要修改使用該行為的物件。

在這個例子中,ICoffeeMaker 代表不同的咖啡製作策略,而 CoffeeMachine 只依賴於這個抽象接口,因此可以輕鬆地切換不同的咖啡製作策略。

3. 這樣設計的好處

3.1 降低耦合性

  • CoffeeMachine 不再直接依賴於具體的咖啡製作類別,而是依賴於 ICoffeeMaker 接口。
  • 當需要新增或修改咖啡製作方式時,只需實現新的 ICoffeeMaker 類別,不需要改動 CoffeeMachine 類別本身。
  • CoffeeMachine 和具體的咖啡製作器之間的耦合大幅降低,這使得代碼更易於擴展和維護。

3.2 擴展性強

  • 新增一個新種類的咖啡,只需要創建一個新的實現 ICoffeeMaker 的類別即可,例如 MochaMaker,而 CoffeeMachine 本身不需要任何改動。
  • 這種方式完全遵循了 開放-封閉原則(OCP),類別對擴展開放(添加新的 ICoffeeMaker 實現),對修改封閉(CoffeeMachine 本身不需要更改)。

3.3 可測試性

  • 由於 CoffeeMachine 依賴於接口而不是具體的類別,在單元測試中,我們可以輕鬆地使用一個模擬(mock)或替代的 ICoffeeMaker 來測試 CoffeeMachine,而不必真正製作咖啡。

4. 總結

在這個優化的設計中,我們通過以下方式使代碼更具靈活性並降低耦合:

  1. 依賴注入(Dependency Injection)CoffeeMachine 不再直接依賴具體的類別,而是依賴於一個抽象接口 ICoffeeMaker,使得它能夠靈活應對不同的咖啡製作行為。
  2. 策略模式(Strategy Pattern):不同的咖啡製作行為被抽象為策略(ICoffeeMaker),並可以在運行時動態更改,從而達到靈活擴展的目的。
  3. 開放-封閉原則(OCP):這種設計完全符合 OCP,對擴展是開放的(可以添加新類別),對修改是封閉的(不必改動 CoffeeMachine 本身)。

這樣設計不僅保持了系統的靈活性,而且大大降低了類別之間的耦合,使得系統能夠更穩定地應對需求變化。如果你還有其他問題,或者需要進一步的幫助,隨時告訴我!