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 格式,開發者只需要:

  1. 理解 ReportFormatter 介面(一個方法)
  2. 實作 format 方法

不需要閱讀 PdfFormatterExcelFormatterReportGenerator 的實作。

擴展時只需要理解介面,不需要理解實作

這就是 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)

開發者只需要:

  1. 理解 LanguageParser 介面
  2. 實作兩個方法
  3. 註冊解析器

不需要閱讀其他解析器的實作。

如何用「降低認知負擔」來判斷設計好壞

面對設計決策時,問自己這些問題:

  1. 擴展時:開發者需要理解多少現有程式碼?

    • 好:只需要理解介面
    • 壞:需要理解整個實作
  2. 修改時:改動會影響多少其他部分?

    • 好:改動是局部的
    • 壞:改動會連鎖反應
  3. 閱讀時:讀者一次需要記住多少概念?

    • 好:一次一個概念
    • 壞:需要同時記住多個概念

設計原則檢查清單

開放封閉原則

  • 新增功能是否不需要修改現有程式碼?
  • 擴展時是否只需要理解介面?
  • 是否有抽象層隔離變化點?

單一職責原則

  • 類別是否只有一個改變的理由?
  • 類別名稱是否能清楚描述其職責?
  • 類別是否小到可以快速理解?

認知負擔檢查

  • 讀者是否能在 5 分鐘內理解這個類別?
  • 是否需要閱讀其他類別才能理解這個類別?
  • 修改這個類別是否需要擔心影響其他部分?

小結

從認知負擔的視角來看,OCP 和 SRP 的核心目的是:

  • OCP:讓開發者不需要理解整個系統就能擴展
  • SRP:讓開發者一次只需要理解一件事

這和命名是同一個哲學:降低閱讀者的認知負擔

當你面對設計決策時,不要問「這是否符合 OCP」,而是問:

下一個開發者要擴展這個功能時,需要理解多少現有程式碼?

答案越少,設計越好。


延伸閱讀

延伸閱讀(進階系列)


參考資料

  • Martin, R. C. (2000). “Design Principles and Design Patterns”
  • Martin, R. C. (2017). “Clean Architecture”
  • Meyer, B. (1988). “Object-Oriented Software Construction”