本章介紹 SOLID 原則中最重要的一項——開放/封閉原則(OCP),由 Bertrand Meyer 於 1988 年提出。OCP 是物件導向設計的核心,遵循此原則所獲得的好處,正是物件導向技術被廣泛採用的根本原因。
原則定義#
軟體實體(類別、模組、函式等)應該對擴展開放,對修改封閉。 (Software entities should be open for extension, but closed for modification.)
這看似矛盾的兩個目標,透過**抽象(Abstraction)**來實現:
- 對擴展開放(Open for Extension):模組的行為可以被擴展,以滿足新的需求
- 對修改封閉(Closed for Modification):擴展行為時,不需要修改模組的現有源碼
違反症狀#
當系統違反 OCP 時:
- 每次新增功能都必須修改現有程式碼
- 一個小變更引發一連串的修改——觸及多個模組
- 系統呈現僵化性(Rigidity):原本應該只是「加東西」的工作,變成了「到處改」
抽象是關鍵#
在 C# 中,可以建立固定的抽象(抽象基底類別或介面),它代表一組無限制的可能行為。模組依賴於這個抽象,因此對修改是封閉的;同時,新的衍生類別可以提供新的行為,因此對擴展是開放的。
STRATEGY 模式#

Figure 9.2: STRATEGY pattern: Client is both open and closed.
Client 依賴 ClientInterface(抽象),而 Server 實作這個介面。當需要新的行為時,只需新增一個實作 ClientInterface 的類別,不需要修改 Client。
TEMPLATE METHOD 模式#
另一種實現 OCP 的方式:基底類別定義演算法的骨架,將可變的步驟宣告為抽象方法,由衍生類別提供具體實作。基底類別對修改封閉,衍生類別提供擴展。
Shape 應用程式範例#
違反 OCP 的程序式做法#
public void DrawAllShapes(IList shapes)
{
foreach (object shape in shapes)
{
if (shape is Square)
DrawSquare((Square)shape);
else if (shape is Circle)
DrawCircle((Circle)shape);
}
}每次新增一種圖形(如三角形),都必須修改 DrawAllShapes——新增一個 else if 分支。這段程式碼對修改不是封閉的。
遵循 OCP 的物件導向做法#
public interface IShape
{
void Draw();
}
public class Square : IShape
{
public void Draw()
{
// 繪製正方形
}
}
public class Circle : IShape
{
public void Draw()
{
// 繪製圓形
}
}
public void DrawAllShapes(IList<IShape> shapes)
{
foreach (IShape shape in shapes)
shape.Draw();
}新增三角形時,只需建立新的 Triangle 類別並實作 IShape——DrawAllShapes 完全不需要修改。
預測變更與「自然」結構#
注意: 沒有任何模型對所有情境都是「自然的」。對一種變更封閉的設計,可能對另一種變更完全開放。例如上述的 OO 解法對「新增圖形種類」封閉,但如果需求是「所有圓形必須在正方形之前繪製」,那現有的設計就違反 OCP 了。
「上當一次」策略#
既然無法預見所有可能的變更,Martin 建議採用**「fool me once」**策略:
- 先寫出最簡單的程式碼
- 當第一次面臨某類變更時,重構程式碼以保護未來同類型的變更
- 不要試圖預測所有變更——「We permit ourselves to be fooled once」
刺激變更的手段#
為了盡早發現需要保護的變更類型:
- 先寫測試(Test First):測試本身就是一種客戶端,迫使你思考如何讓程式碼可測試,進而促進解耦
- 短迭代週期(Short Cycles):每隔數天就交付功能,讓問題盡早浮現
- 先開發功能再開發基礎設施:讓基礎設施服務於實際需求,而非假設的需求
- 頻繁向利害關係人展示:盡早獲得回饋
使用抽象達成明確封閉#
當排序需求出現(例如圓形必須在正方形之前繪製)時,可以讓 Shape 實作 IComparable:
public interface IShape : IComparable<IShape>
{
void Draw();
}但這樣每個 Shape 都需要知道排序邏輯,不夠靈活。
資料驅動的做法#
更好的方式是使用資料驅動(Data-Driven)的封閉策略,將排序規則抽離到外部資料表:
public class ShapeComparer : IComparer<IShape>
{
private static Dictionary<Type, int> priorities = new Dictionary<Type, int>();
static ShapeComparer()
{
priorities[typeof(Circle)] = 1;
priorities[typeof(Square)] = 2;
}
public int Compare(IShape s1, IShape s2)
{
int p1 = priorities[s1.GetType()];
int p2 = priorities[s2.GetType()];
return p1.CompareTo(p2);
}
}這樣新增圖形種類時,只需在優先權表中加入一行,不需要修改任何現有的 Shape 類別或 DrawAllShapes。DrawAllShapes 與各 Shape 類別都對繪製順序的變更保持封閉。
重點: 完全的封閉是不可能的。設計者必須策略性地選擇對哪些變更封閉。經驗、敏銳度和對使用者及產業的了解,是做出正確判斷的關鍵。
本章小結#
OCP 是物件導向設計中最核心的原則。抵抗過早的抽象與善用抽象同樣重要——在沒有看到變更模式之前就抽象化,不但無法防止未來的問題,反而會引入不必要的複雜性。敏捷開發者只在臭味出現時才行動,利用測試、短迭代與持續重構,在最適當的時機引入最適當的抽象。