開放封閉原則與認知負擔
OCP 的傳統解釋
開放封閉原則(Open-Closed Principle, OCP)是 SOLID 原則之一,傳統定義是:
軟體實體(類別、模組、函式)應該對擴展開放,對修改封閉。
這意味著:
- 對擴展開放:可以增加新功能
- 對修改封閉:不需要修改現有程式碼
傳統焦點:避免修改帶來的風險
傳統觀點認為,OCP 的目的是:
- 避免修改穩定的程式碼引入錯誤
- 減少回歸測試的範圍
- 保護現有功能不受影響
這些都是正確的,但還有一個更深層的目的。
OCP 的認知負擔視角(用戶觀點)
真正目的:讓閱讀者不需要理解整個系統才能使用
從認知負擔的角度來看,OCP 的核心價值是:
擴展系統時,開發者只需要理解介面,不需要理解實作。
這大幅降低了認知負擔。
範例:違反 OCP 的設計
1class ReportGenerator:
2 def generate(self, report_type: str, data: dict) -> str:
3 if report_type == "pdf":
4 # 100 行 PDF 生成邏輯
5 return self._generate_pdf(data)
6 elif report_type == "excel":
7 # 80 行 Excel 生成邏輯
8 return self._generate_excel(data)
9 elif report_type == "html":
10 # 60 行 HTML 生成邏輯
11 return self._generate_html(data)
12 else:
13 raise ValueError(f"Unknown report type: {report_type}")問題:
- 新增格式需要修改這個類別
- 理解新增功能需要閱讀整個類別
- 每種格式的邏輯混在一起
認知負擔:要新增 CSV 格式,開發者需要理解整個 ReportGenerator 類別的結構,找到正確的位置插入程式碼,並確保不影響其他格式。
範例:遵循 OCP 的設計
1from abc import ABC, abstractmethod
2
3class ReportFormatter(ABC):
4 """報告格式化器的抽象介面"""
5
6 @abstractmethod
7 def format(self, data: dict) -> str:
8 """將資料格式化為報告"""
9 pass
10
11class PdfFormatter(ReportFormatter):
12 def format(self, data: dict) -> str:
13 # PDF 生成邏輯
14 pass
15
16class ExcelFormatter(ReportFormatter):
17 def format(self, data: dict) -> str:
18 # Excel 生成邏輯
19 pass
20
21class HtmlFormatter(ReportFormatter):
22 def format(self, data: dict) -> str:
23 # HTML 生成邏輯
24 pass
25
26class ReportGenerator:
27 def __init__(self, formatter: ReportFormatter):
28 self._formatter = formatter
29
30 def generate(self, data: dict) -> str:
31 return self._formatter.format(data)新增 CSV 格式:
1class CsvFormatter(ReportFormatter):
2 def format(self, data: dict) -> str:
3 # CSV 生成邏輯
4 pass
5
6# 使用
7generator = ReportGenerator(CsvFormatter())
8report = generator.generate(data)認知負擔:要新增 CSV 格式,開發者只需要:
- 理解
ReportFormatter介面(一個方法) - 實作
format方法
不需要閱讀 PdfFormatter、ExcelFormatter 或 ReportGenerator 的實作。
擴展時只需要理解介面,不需要理解實作
這就是 OCP 降低認知負擔的方式:
| 情境 | 違反 OCP | 遵循 OCP |
|---|---|---|
| 新增格式 | 需要理解整個類別 | 只需理解介面 |
| 修改一個格式 | 可能影響其他格式 | 完全隔離 |
| 閱讀程式碼 | 需要跟蹤 if-else 分支 | 直接看對應的類別 |
這和命名是同一件事:降低認知負擔
回想命名的藝術:好的命名讓讀者不需要追溯定義就能理解。
OCP 做的是同樣的事,只是在更高的層級:好的設計讓讀者不需要理解整個系統就能擴展。
單一職責原則的本質
單一職責原則(Single Responsibility Principle, SRP)是另一個 SOLID 原則:
一個類別應該只有一個改變的理由。
一次只理解一件事
從認知負擔的角度,SRP 的核心是:
讓讀者一次只需要理解一件事。
1# 違反 SRP:一個類別做太多事
2class UserManager:
3 def create_user(self, data):
4 # 驗證邏輯
5 # 資料庫操作
6 # 發送歡迎郵件
7 # 記錄日誌
8 pass
9
10 def delete_user(self, user_id):
11 # 權限檢查
12 # 資料庫操作
13 # 清理關聯資料
14 # 發送通知
15 # 記錄日誌
16 pass問題:讀者想理解「如何發送歡迎郵件」,卻需要閱讀整個 UserManager 類別。
1# 遵循 SRP:每個類別只做一件事
2class UserValidator:
3 def validate(self, data: dict) -> ValidationResult:
4 pass
5
6class UserRepository:
7 def create(self, user: User) -> User:
8 pass
9
10 def delete(self, user_id: str) -> bool:
11 pass
12
13class EmailService:
14 def send_welcome_email(self, user: User) -> None:
15 pass
16
17class UserService:
18 def __init__(
19 self,
20 validator: UserValidator,
21 repository: UserRepository,
22 email_service: EmailService
23 ):
24 self._validator = validator
25 self._repository = repository
26 self._email_service = email_service
27
28 def create_user(self, data: dict) -> User:
29 validation = self._validator.validate(data)
30 if not validation.is_valid:
31 raise ValidationError(validation.errors)
32
33 user = self._repository.create(User.from_dict(data))
34 self._email_service.send_welcome_email(user)
35 return user現在,讀者想理解「如何發送歡迎郵件」,只需要看 EmailService。
類別/函式的職責清晰 = 閱讀時認知負擔低
| 職責數量 | 認知負擔 | 維護難度 |
|---|---|---|
| 1 | 低 | 容易 |
| 2-3 | 中 | 需要注意 |
| 4+ | 高 | 危險區域 |
和命名的關聯:如果難以命名,可能職責不單一
這是一個非常實用的檢測方法:
1# 難以命名 = 職責不單一
2class UserStuffManager: # "stuff" 說明不知道它具體做什麼
3 pass
4
5class DataProcessorAndValidator: # "and" 說明做了兩件事
6 pass
7
8class HelperUtils: # "helper/utils" 說明是雜項收集
9 pass
10
11# 容易命名 = 職責單一
12class UserAuthenticator: # 清楚:處理用戶認證
13 pass
14
15class ConfigurationLoader: # 清楚:載入配置
16 pass
17
18class EmailFormatter: # 清楚:格式化郵件
19 pass規則:如果你無法用一個簡短的名詞描述類別的職責,它可能做了太多事。
實際案例
Hook 系統中的設計決策
讓我們看 Hook 系統中如何應用這些原則:
配置載入器(遵循 SRP)
1# .claude/lib/config_loader.py
2class ConfigLoader:
3 """
4 單一職責:載入和快取配置檔案
5
6 不負責:
7 - 驗證配置內容
8 - 使用配置執行操作
9 - 修改配置
10 """
11
12 def __init__(self, config_path: Path):
13 self._config_path = config_path
14 self._cache: Optional[dict] = None
15
16 def load(self) -> dict:
17 """載入配置,使用快取避免重複讀取"""
18 if self._cache is None:
19 self._cache = self._read_config()
20 return self._cache
21
22 def _read_config(self) -> dict:
23 with open(self._config_path) as f:
24 return yaml.safe_load(f)解析器工廠(遵循 OCP)
1# .claude/lib/parsers/base.py
2class LanguageParser(ABC):
3 """語言解析器的抽象介面"""
4
5 @abstractmethod
6 def parse(self, content: str) -> ParseResult:
7 pass
8
9 @abstractmethod
10 def get_supported_extensions(self) -> list[str]:
11 pass
12
13# .claude/lib/parsers/python_parser.py
14class PythonParser(LanguageParser):
15 def parse(self, content: str) -> ParseResult:
16 # Python 解析邏輯
17 pass
18
19 def get_supported_extensions(self) -> list[str]:
20 return [".py"]
21
22# .claude/lib/parsers/factory.py
23class ParserFactory:
24 """
25 對擴展開放:新增語言只需實作 LanguageParser
26 對修改封閉:不需要修改此工廠類別
27 """
28
29 _parsers: dict[str, type[LanguageParser]] = {}
30
31 @classmethod
32 def register(cls, parser_class: type[LanguageParser]) -> None:
33 for ext in parser_class.get_supported_extensions():
34 cls._parsers[ext] = parser_class
35
36 @classmethod
37 def get_parser(cls, file_extension: str) -> LanguageParser:
38 parser_class = cls._parsers.get(file_extension)
39 if parser_class is None:
40 raise UnsupportedLanguageError(file_extension)
41 return parser_class()新增 Dart 語言支援:
1# .claude/lib/parsers/dart_parser.py
2class DartParser(LanguageParser):
3 def parse(self, content: str) -> ParseResult:
4 # Dart 解析邏輯
5 pass
6
7 def get_supported_extensions(self) -> list[str]:
8 return [".dart"]
9
10# 註冊(可在初始化時自動完成)
11ParserFactory.register(DartParser)開發者只需要:
- 理解
LanguageParser介面 - 實作兩個方法
- 註冊解析器
不需要閱讀其他解析器的實作。
如何用「降低認知負擔」來判斷設計好壞
面對設計決策時,問自己這些問題:
擴展時:開發者需要理解多少現有程式碼?
- 好:只需要理解介面
- 壞:需要理解整個實作
修改時:改動會影響多少其他部分?
- 好:改動是局部的
- 壞:改動會連鎖反應
閱讀時:讀者一次需要記住多少概念?
- 好:一次一個概念
- 壞:需要同時記住多個概念
設計原則檢查清單
開放封閉原則
- 新增功能是否不需要修改現有程式碼?
- 擴展時是否只需要理解介面?
- 是否有抽象層隔離變化點?
單一職責原則
- 類別是否只有一個改變的理由?
- 類別名稱是否能清楚描述其職責?
- 類別是否小到可以快速理解?
認知負擔檢查
- 讀者是否能在 5 分鐘內理解這個類別?
- 是否需要閱讀其他類別才能理解這個類別?
- 修改這個類別是否需要擔心影響其他部分?
小結
從認知負擔的視角來看,OCP 和 SRP 的核心目的是:
- OCP:讓開發者不需要理解整個系統就能擴展
- SRP:讓開發者一次只需要理解一件事
這和命名是同一個哲學:降低閱讀者的認知負擔。
當你面對設計決策時,不要問「這是否符合 OCP」,而是問:
下一個開發者要擴展這個功能時,需要理解多少現有程式碼?
答案越少,設計越好。
延伸閱讀
- 認知負擔:程式碼設計的核心目的 - 認知負擔的基本概念
- 命名的藝術:讓程式碼說故事 - 降低認知負擔的另一種方式
- 抽象基類 ABC - Python 中實現 OCP 的工具
延伸閱讀(進階系列)
參考資料
- Martin, R. C. (2000). “Design Principles and Design Patterns”
- Martin, R. C. (2017). “Clean Architecture”
- Meyer, B. (1988). “Object-Oriented Software Construction”