Hook 系統採用 (bool, str) 返回值模式,這是一種替代異常處理的設計策略。本章深入探討這個模式的設計理念和最佳實踐。

(bool, str) 模式

基本形式

 1def validate_something() -> tuple[bool, str]:
 2    """
 3    Returns:
 4        tuple[bool, str]:
 5            - (True, "成功訊息") - 操作成功
 6            - (False, "錯誤訊息") - 操作失敗
 7    """
 8    if some_condition:
 9        return True, "驗證通過"
10    return False, "驗證失敗:原因說明"

使用方式

1success, message = validate_something()
2if success:
3    print(f"成功: {message}")
4else:
5    print(f"失敗: {message}")
6    # 決定如何處理錯誤

實際範例:Git 命令

來自 .claude/lib/git_utils.py

 1def run_git_command(
 2    args: list[str],
 3    cwd: Optional[str] = None,
 4    timeout: int = 10
 5) -> tuple[bool, str]:
 6    """
 7    執行 git 命令並返回結果
 8
 9    Args:
10        args: git 命令參數列表
11        cwd: 執行目錄
12        timeout: 超時時間(秒)
13
14    Returns:
15        tuple[bool, str]: (是否成功, 輸出或錯誤訊息)
16    """
17    try:
18        result = subprocess.run(
19            ["git"] + args,
20            cwd=cwd,
21            capture_output=True,
22            text=True,
23            timeout=timeout
24        )
25        if result.returncode == 0:
26            return True, result.stdout.strip()
27        else:
28            return False, result.stderr.strip()
29    except subprocess.TimeoutExpired:
30        return False, f"Command timed out after {timeout}s"
31    except FileNotFoundError:
32        return False, "git command not found"

使用這個函式

1def get_current_branch() -> Optional[str]:
2    """獲取當前分支名稱"""
3    success, output = run_git_command(["branch", "--show-current"])
4    return output if success and output else None
5
6def get_project_root() -> str:
7    """獲取專案根目錄"""
8    success, output = run_git_command(["rev-parse", "--show-toplevel"])
9    return output if success else os.getcwd()

為什麼選擇這個模式?

優點

明確的錯誤處理

1# 呼叫者必須處理兩種情況
2success, message = validate()
3if not success:
4    # 必須處理錯誤
5    pass

錯誤訊息保留

1# 錯誤訊息可以傳遞給使用者
2success, error = run_command()
3if not success:
4    logger.error(error)
5    return create_error_response(error)

不中斷執行流程

1# 可以收集所有錯誤
2errors = []
3for file in files:
4    success, message = process(file)
5    if not success:
6        errors.append(message)

與異常的比較

特性(bool, str)異常
強制處理是(需要解包)否(可能被忽略)
控制流程不中斷中斷
效能較好較差(stack trace)
適合場景預期的錯誤非預期的錯誤

設計變體

(bool, T) - 通用型別

1from typing import TypeVar, Tuple
2
3T = TypeVar("T")
4
5def parse_json(text: str) -> Tuple[bool, dict]:
6    try:
7        return True, json.loads(text)
8    except json.JSONDecodeError as e:
9        return False, {"error": str(e)}

(T | None) - 成功返回值,失敗返回 None

1def find_config(name: str) -> Optional[dict]:
2    """找不到時返回 None"""
3    path = get_config_path(name)
4    if not path.exists():
5        return None
6    return load_config(path)

Result 類別(進階)

 1from dataclasses import dataclass
 2from typing import Generic, TypeVar
 3
 4T = TypeVar("T")
 5
 6@dataclass
 7class Result(Generic[T]):
 8    success: bool
 9    value: Optional[T] = None
10    error: Optional[str] = None
11
12    @classmethod
13    def ok(cls, value: T) -> "Result[T]":
14        return cls(success=True, value=value)
15
16    @classmethod
17    def fail(cls, error: str) -> "Result[T]":
18        return cls(success=False, error=error)
19
20# 使用
21def divide(a: int, b: int) -> Result[float]:
22    if b == 0:
23        return Result.fail("Division by zero")
24    return Result.ok(a / b)
25
26result = divide(10, 2)
27if result.success:
28    print(result.value)  # 5.0
29else:
30    print(result.error)

實際應用模式

鏈式操作

 1def process_pipeline(data: str) -> Tuple[bool, str]:
 2    # 第一步
 3    success, result = step_one(data)
 4    if not success:
 5        return False, f"Step 1 failed: {result}"
 6
 7    # 第二步
 8    success, result = step_two(result)
 9    if not success:
10        return False, f"Step 2 failed: {result}"
11
12    # 第三步
13    success, result = step_three(result)
14    if not success:
15        return False, f"Step 3 failed: {result}"
16
17    return True, result

收集多個錯誤

 1def validate_all(items: list[str]) -> Tuple[bool, list[str]]:
 2    errors = []
 3    for item in items:
 4        success, message = validate_item(item)
 5        if not success:
 6            errors.append(message)
 7
 8    if errors:
 9        return False, errors
10    return True, []

帶上下文的錯誤

 1def run_with_context(
 2    command: str
 3) -> Tuple[bool, str]:
 4    """執行命令並提供上下文"""
 5    try:
 6        result = execute(command)
 7        return True, result
 8    except Exception as e:
 9        context = f"Command: {command}\nError: {e}"
10        return False, context

Hook 系統的應用

Hook 輸入讀取

1def read_hook_input() -> dict:
2    """
3    從 stdin 讀取輸入
4    失敗時返回空字典(而非拋出異常)
5    """
6    try:
7        return json.load(sys.stdin)
8    except json.JSONDecodeError:
9        return {}

分支驗證

1def validate_branch(branch: str) -> Tuple[bool, str]:
2    """驗證分支是否可以編輯"""
3    if is_protected_branch(branch):
4        return False, f"Cannot edit protected branch: {branch}"
5    if not is_allowed_branch(branch):
6        return False, f"Branch not in allowed list: {branch}"
7    return True, f"Branch {branch} is valid"

最佳實踐

1. 訊息要有意義

1# 好:提供具體資訊
2return False, f"Config file not found: {path}"
3
4# 不好:模糊的訊息
5return False, "Error"

2. 保持一致性

1# 好:整個模組使用相同模式
2def func_a() -> Tuple[bool, str]: ...
3def func_b() -> Tuple[bool, str]: ...
4def func_c() -> Tuple[bool, str]: ...
5
6# 不好:混合使用
7def func_a() -> Tuple[bool, str]: ...
8def func_b() -> Optional[str]: ...  # 不一致
9def func_c(): ...  # 可能拋出異常

3. 文檔說明返回值

 1def validate(data: str) -> Tuple[bool, str]:
 2    """
 3    驗證資料格式
 4
 5    Returns:
 6        tuple[bool, str]:
 7            - (True, "Validation passed") - 驗證成功
 8            - (False, error_message) - 驗證失敗,包含原因
 9    """
10    ...

思考題

  1. 什麼情況下應該使用 (bool, str) 而不是異常?
  2. 如何處理需要返回多種錯誤類型的情況?
  3. (bool, str) 模式如何與型別檢查工具配合?

實作練習

  1. 重構一個使用異常的函式,改為 (bool, str) 模式
  2. 實作一個 Result 類別,支援 mapflat_map 操作
  3. 寫一個函式,執行多個驗證並收集所有錯誤

上一章:異常處理策略 下一章:unittest 基礎