本案例基於 .claude/lib/hook_validator.py 的實際程式碼,展示如何用 Generic 和 TypeVar 建立型別安全的通用驗證器。

先備知識

問題背景

現有設計

hook_validator.py 針對特定類型設計:

 1from dataclasses import dataclass, field
 2from pathlib import Path
 3from typing import Optional, List
 4
 5@dataclass
 6class ValidationIssue:
 7    """Single validation issue"""
 8    level: str  # "error" | "warning" | "info"
 9    message: str
10    line: Optional[int] = None
11    suggestion: Optional[str] = None
12
13@dataclass
14class ValidationResult:
15    """Validation result for a single hook"""
16    hook_path: str
17    issues: List[ValidationIssue] = field(default_factory=list)
18    is_compliant: bool = True
19
20    def __post_init__(self):
21        """Calculate is_compliant status"""
22        self.is_compliant = not any(
23            issue.level == "error" for issue in self.issues
24        )
25
26class HookValidator:
27    """Hook compliance validator - specific to Path type"""
28
29    def validate_hook(self, hook_path: str) -> ValidationResult:
30        """Validate a single hook file"""
31        hook_path = self._resolve_path(hook_path)
32
33        if not hook_path.exists():
34            return ValidationResult(
35                hook_path=str(hook_path),
36                issues=[ValidationIssue(
37                    level="error",
38                    message=f"Hook file not found: {hook_path}"
39                )]
40            )
41
42        # ... more validation logic ...
43        return ValidationResult(hook_path=str(hook_path))
44
45    def _resolve_path(self, path: str) -> Path:
46        """Resolve path to absolute path"""
47        p = Path(path)
48        return p if p.is_absolute() else Path.cwd() / p

這個設計的優點

  • 針對具體需求設計:專為驗證 Hook 檔案設計,邏輯清晰
  • 型別明確:輸入是 str,輸出是 ValidationResult

這個設計的限制

當需要驗證其他類型時:

  • 需要複製大量相似程式碼:如果要驗證 API 回應、設定檔、表單輸入,需要寫多個類似的 Validator
  • 驗證邏輯無法重用:「檢查是否為空」「檢查格式」這些通用邏輯無法跨 Validator 共享
  • 型別檢查不夠通用ValidationResult 綁定了 hook_path: str,無法用於其他場景

進階解決方案

設計目標

  1. 建立通用的驗證器介面:定義 Validator[T] 協議
  2. 支援任意輸入類型:可以驗證 Pathstrdict、自訂類別
  3. 保持型別安全:靜態型別檢查器能捕捉型別錯誤
  4. 支援驗證器組合:用 AndOrNot 組合基本驗證器

實作步驟

步驟 1:定義泛型 Validator 協議

首先,我們需要定義「什麼是驗證器」。使用 Protocol 和 TypeVar 來建立泛型介面:

 1from typing import Protocol, TypeVar, Generic
 2from dataclasses import dataclass, field
 3from abc import abstractmethod
 4
 5# Define a type variable for the input type
 6T = TypeVar("T")
 7
 8# Type variable with contravariance for Protocol
 9T_contra = TypeVar("T_contra", contravariant=True)
10
11@dataclass
12class ValidationResult(Generic[T]):
13    """
14    Generic validation result
15
16    Type parameter T represents the validated value type.
17    This allows type-safe access to the validated value.
18    """
19    value: T
20    is_valid: bool = True
21    errors: list[str] = field(default_factory=list)
22    warnings: list[str] = field(default_factory=list)
23
24    def add_error(self, message: str) -> "ValidationResult[T]":
25        """Add an error and mark as invalid"""
26        self.errors.append(message)
27        self.is_valid = False
28        return self
29
30    def add_warning(self, message: str) -> "ValidationResult[T]":
31        """Add a warning (does not affect validity)"""
32        self.warnings.append(message)
33        return self
34
35class Validator(Protocol[T_contra]):
36    """
37    Generic validator protocol
38
39    Any class that implements validate(value: T) -> ValidationResult[T]
40    is considered a Validator[T].
41
42    Using contravariant type variable because validators consume values.
43    A Validator[Animal] can validate Dog (subtype), so it's contravariant.
44    """
45
46    def validate(self, value: T_contra) -> ValidationResult:
47        """Validate the given value and return the result"""
48        ...

關鍵設計決策

  • T_contra 使用逆變(contravariant),因為驗證器是「消費者」
  • ValidationResult[T] 是泛型,讓結果可以攜帶原始值的型別資訊
  • Protocol 而非 ABC,支援結構型子型別(不需要顯式繼承)

步驟 2:實作具體驗證器

接下來實作幾個常用的基礎驗證器:

 1from pathlib import Path
 2import re
 3
 4class NotEmptyValidator:
 5    """Validate that a string is not empty"""
 6
 7    def validate(self, value: str) -> ValidationResult[str]:
 8        result = ValidationResult(value=value)
 9        if not value or not value.strip():
10            result.add_error("Value cannot be empty")
11        return result
12
13class PathExistsValidator:
14    """Validate that a path exists"""
15
16    def __init__(self, must_be_file: bool = False, must_be_dir: bool = False):
17        self.must_be_file = must_be_file
18        self.must_be_dir = must_be_dir
19
20    def validate(self, value: Path) -> ValidationResult[Path]:
21        result = ValidationResult(value=value)
22
23        if not value.exists():
24            result.add_error(f"Path does not exist: {value}")
25        elif self.must_be_file and not value.is_file():
26            result.add_error(f"Path is not a file: {value}")
27        elif self.must_be_dir and not value.is_dir():
28            result.add_error(f"Path is not a directory: {value}")
29
30        return result
31
32class PatternValidator:
33    """Validate that a string matches a regex pattern"""
34
35    def __init__(self, pattern: str, error_message: str | None = None):
36        self.pattern = re.compile(pattern)
37        self.error_message = error_message or f"Value must match pattern: {pattern}"
38
39    def validate(self, value: str) -> ValidationResult[str]:
40        result = ValidationResult(value=value)
41        if not self.pattern.match(value):
42            result.add_error(self.error_message)
43        return result
44
45class RangeValidator:
46    """Validate that a number is within a range"""
47
48    def __init__(
49        self,
50        min_value: float | None = None,
51        max_value: float | None = None
52    ):
53        self.min_value = min_value
54        self.max_value = max_value
55
56    def validate(self, value: float | int) -> ValidationResult[float | int]:
57        result = ValidationResult(value=value)
58
59        if self.min_value is not None and value < self.min_value:
60            result.add_error(f"Value {value} is below minimum {self.min_value}")
61        if self.max_value is not None and value > self.max_value:
62            result.add_error(f"Value {value} is above maximum {self.max_value}")
63
64        return result

步驟 3:驗證器組合(And、Or、Not)

驗證器的威力來自於組合。實作三個組合器:

 1from typing import Sequence
 2
 3class AndValidator(Generic[T]):
 4    """
 5    Combine multiple validators with AND logic
 6
 7    All validators must pass for the result to be valid.
 8    """
 9
10    def __init__(self, validators: Sequence[Validator[T]]):
11        self.validators = validators
12
13    def validate(self, value: T) -> ValidationResult[T]:
14        result = ValidationResult(value=value)
15
16        for validator in self.validators:
17            sub_result = validator.validate(value)
18            result.errors.extend(sub_result.errors)
19            result.warnings.extend(sub_result.warnings)
20
21        result.is_valid = len(result.errors) == 0
22        return result
23
24class OrValidator(Generic[T]):
25    """
26    Combine multiple validators with OR logic
27
28    At least one validator must pass for the result to be valid.
29    """
30
31    def __init__(self, validators: Sequence[Validator[T]]):
32        self.validators = validators
33
34    def validate(self, value: T) -> ValidationResult[T]:
35        result = ValidationResult(value=value)
36        all_errors: list[str] = []
37
38        for validator in self.validators:
39            sub_result = validator.validate(value)
40            if sub_result.is_valid:
41                # At least one passed, return success
42                result.warnings.extend(sub_result.warnings)
43                return result
44            all_errors.extend(sub_result.errors)
45
46        # All failed
47        result.add_error(
48            f"None of the validators passed. Errors: {'; '.join(all_errors)}"
49        )
50        return result
51
52class NotValidator(Generic[T]):
53    """
54    Negate a validator
55
56    The result is valid if the inner validator fails.
57    """
58
59    def __init__(self, validator: Validator[T], error_message: str | None = None):
60        self.validator = validator
61        self.error_message = error_message or "Validation should have failed"
62
63    def validate(self, value: T) -> ValidationResult[T]:
64        result = ValidationResult(value=value)
65        sub_result = self.validator.validate(value)
66
67        if sub_result.is_valid:
68            result.add_error(self.error_message)
69        # If inner failed, outer succeeds
70        return result

步驟 4:型別安全的建構器

為了讓組合更流暢,加入建構器模式:

 1from typing import Callable
 2
 3class ValidatorBuilder(Generic[T]):
 4    """
 5    Fluent builder for composing validators
 6
 7    Provides a chainable API for building complex validators.
 8    """
 9
10    def __init__(self):
11        self._validators: list[Validator[T]] = []
12
13    def add(self, validator: Validator[T]) -> "ValidatorBuilder[T]":
14        """Add a validator to the chain"""
15        self._validators.append(validator)
16        return self
17
18    def add_if(
19        self,
20        condition: bool,
21        validator: Validator[T]
22    ) -> "ValidatorBuilder[T]":
23        """Conditionally add a validator"""
24        if condition:
25            self._validators.append(validator)
26        return self
27
28    def build(self) -> Validator[T]:
29        """Build the final AND-combined validator"""
30        if len(self._validators) == 1:
31            return self._validators[0]
32        return AndValidator(self._validators)
33
34    def build_or(self) -> Validator[T]:
35        """Build with OR logic instead of AND"""
36        if len(self._validators) == 1:
37            return self._validators[0]
38        return OrValidator(self._validators)
39
40def validator_for(type_hint: type[T]) -> ValidatorBuilder[T]:
41    """
42    Create a type-safe validator builder
43
44    Usage:
45        validator = (
46            validator_for(str)
47            .add(NotEmptyValidator())
48            .add(PatternValidator(r"^[a-z]+$"))
49            .build()
50        )
51    """
52    return ValidatorBuilder[T]()

完整程式碼

  1#!/usr/bin/env python3
  2"""
  3Generic Validator System
  4
  5A type-safe, composable validation framework using Generic and TypeVar.
  6"""
  7
  8from __future__ import annotations
  9from abc import abstractmethod
 10from dataclasses import dataclass, field
 11from pathlib import Path
 12from typing import (
 13    Callable,
 14    Generic,
 15    Protocol,
 16    Sequence,
 17    TypeVar,
 18    runtime_checkable,
 19)
 20import re
 21
 22# ===== Type Variables =====
 23
 24T = TypeVar("T")
 25T_contra = TypeVar("T_contra", contravariant=True)
 26
 27# ===== Core Types =====
 28
 29@dataclass
 30class ValidationResult(Generic[T]):
 31    """
 32    Generic validation result
 33
 34    Attributes:
 35        value: The validated value (preserves type information)
 36        is_valid: Whether validation passed
 37        errors: List of error messages (cause validation failure)
 38        warnings: List of warning messages (informational only)
 39    """
 40    value: T
 41    is_valid: bool = True
 42    errors: list[str] = field(default_factory=list)
 43    warnings: list[str] = field(default_factory=list)
 44
 45    def add_error(self, message: str) -> ValidationResult[T]:
 46        """Add an error and mark as invalid"""
 47        self.errors.append(message)
 48        self.is_valid = False
 49        return self
 50
 51    def add_warning(self, message: str) -> ValidationResult[T]:
 52        """Add a warning (does not affect validity)"""
 53        self.warnings.append(message)
 54        return self
 55
 56    def __bool__(self) -> bool:
 57        """Allow using result in boolean context"""
 58        return self.is_valid
 59
 60@runtime_checkable
 61class Validator(Protocol[T_contra]):
 62    """
 63    Generic validator protocol
 64
 65    Any class implementing validate(value) -> ValidationResult
 66    satisfies this protocol.
 67    """
 68
 69    def validate(self, value: T_contra) -> ValidationResult:
 70        """Validate the given value"""
 71        ...
 72
 73# ===== Basic Validators =====
 74
 75class NotEmptyValidator:
 76    """Validate that a string is not empty"""
 77
 78    def validate(self, value: str) -> ValidationResult[str]:
 79        result = ValidationResult(value=value)
 80        if not value or not value.strip():
 81            result.add_error("Value cannot be empty")
 82        return result
 83
 84class PathExistsValidator:
 85    """Validate that a path exists"""
 86
 87    def __init__(
 88        self,
 89        must_be_file: bool = False,
 90        must_be_dir: bool = False
 91    ):
 92        self.must_be_file = must_be_file
 93        self.must_be_dir = must_be_dir
 94
 95    def validate(self, value: Path) -> ValidationResult[Path]:
 96        result = ValidationResult(value=value)
 97
 98        if not value.exists():
 99            result.add_error(f"Path does not exist: {value}")
100        elif self.must_be_file and not value.is_file():
101            result.add_error(f"Path is not a file: {value}")
102        elif self.must_be_dir and not value.is_dir():
103            result.add_error(f"Path is not a directory: {value}")
104
105        return result
106
107class PatternValidator:
108    """Validate string matches a regex pattern"""
109
110    def __init__(self, pattern: str, error_message: str | None = None):
111        self.pattern = re.compile(pattern)
112        self.error_message = error_message or f"Must match pattern: {pattern}"
113
114    def validate(self, value: str) -> ValidationResult[str]:
115        result = ValidationResult(value=value)
116        if not self.pattern.match(value):
117            result.add_error(self.error_message)
118        return result
119
120class RangeValidator:
121    """Validate number is within range"""
122
123    def __init__(
124        self,
125        min_value: float | None = None,
126        max_value: float | None = None
127    ):
128        self.min_value = min_value
129        self.max_value = max_value
130
131    def validate(self, value: float | int) -> ValidationResult[float | int]:
132        result = ValidationResult(value=value)
133        if self.min_value is not None and value < self.min_value:
134            result.add_error(f"Value {value} < minimum {self.min_value}")
135        if self.max_value is not None and value > self.max_value:
136            result.add_error(f"Value {value} > maximum {self.max_value}")
137        return result
138
139class LengthValidator:
140    """Validate string length"""
141
142    def __init__(
143        self,
144        min_length: int | None = None,
145        max_length: int | None = None
146    ):
147        self.min_length = min_length
148        self.max_length = max_length
149
150    def validate(self, value: str) -> ValidationResult[str]:
151        result = ValidationResult(value=value)
152        length = len(value)
153
154        if self.min_length is not None and length < self.min_length:
155            result.add_error(f"Length {length} < minimum {self.min_length}")
156        if self.max_length is not None and length > self.max_length:
157            result.add_error(f"Length {length} > maximum {self.max_length}")
158
159        return result
160
161class TypeValidator(Generic[T]):
162    """Validate value is of expected type"""
163
164    def __init__(self, expected_type: type[T], type_name: str | None = None):
165        self.expected_type = expected_type
166        self.type_name = type_name or expected_type.__name__
167
168    def validate(self, value: object) -> ValidationResult[T]:
169        if isinstance(value, self.expected_type):
170            return ValidationResult(value=value)  # type: ignore
171        else:
172            result = ValidationResult(value=value)  # type: ignore
173            result.add_error(
174                f"Expected {self.type_name}, got {type(value).__name__}"
175            )
176            return result
177
178# ===== Composite Validators =====
179
180class AndValidator(Generic[T]):
181    """Combine validators with AND logic (all must pass)"""
182
183    def __init__(self, validators: Sequence[Validator[T]]):
184        self.validators = list(validators)
185
186    def validate(self, value: T) -> ValidationResult[T]:
187        result = ValidationResult(value=value)
188
189        for validator in self.validators:
190            sub_result = validator.validate(value)
191            result.errors.extend(sub_result.errors)
192            result.warnings.extend(sub_result.warnings)
193
194        result.is_valid = len(result.errors) == 0
195        return result
196
197class OrValidator(Generic[T]):
198    """Combine validators with OR logic (at least one must pass)"""
199
200    def __init__(self, validators: Sequence[Validator[T]]):
201        self.validators = list(validators)
202
203    def validate(self, value: T) -> ValidationResult[T]:
204        result = ValidationResult(value=value)
205        all_errors: list[str] = []
206
207        for validator in self.validators:
208            sub_result = validator.validate(value)
209            if sub_result.is_valid:
210                result.warnings.extend(sub_result.warnings)
211                return result
212            all_errors.extend(sub_result.errors)
213
214        result.add_error(f"No validator passed: {'; '.join(all_errors)}")
215        return result
216
217class NotValidator(Generic[T]):
218    """Negate a validator (passes if inner validator fails)"""
219
220    def __init__(self, validator: Validator[T], error_message: str | None = None):
221        self.validator = validator
222        self.error_message = error_message or "Validation should have failed"
223
224    def validate(self, value: T) -> ValidationResult[T]:
225        result = ValidationResult(value=value)
226        sub_result = self.validator.validate(value)
227
228        if sub_result.is_valid:
229            result.add_error(self.error_message)
230
231        return result
232
233# ===== Builder =====
234
235class ValidatorBuilder(Generic[T]):
236    """Fluent builder for composing validators"""
237
238    def __init__(self):
239        self._validators: list[Validator[T]] = []
240
241    def add(self, validator: Validator[T]) -> ValidatorBuilder[T]:
242        """Add a validator"""
243        self._validators.append(validator)
244        return self
245
246    def add_if(
247        self,
248        condition: bool,
249        validator: Validator[T]
250    ) -> ValidatorBuilder[T]:
251        """Conditionally add a validator"""
252        if condition:
253            self._validators.append(validator)
254        return self
255
256    def build(self) -> Validator[T]:
257        """Build AND-combined validator"""
258        if len(self._validators) == 0:
259            raise ValueError("No validators added")
260        if len(self._validators) == 1:
261            return self._validators[0]
262        return AndValidator(self._validators)
263
264    def build_or(self) -> Validator[T]:
265        """Build OR-combined validator"""
266        if len(self._validators) == 0:
267            raise ValueError("No validators added")
268        if len(self._validators) == 1:
269            return self._validators[0]
270        return OrValidator(self._validators)
271
272def validator_for(type_hint: type[T]) -> ValidatorBuilder[T]:
273    """Create a type-safe validator builder"""
274    return ValidatorBuilder[T]()
275
276# ===== List Validator =====
277
278class ListValidator(Generic[T]):
279    """Validate each element in a list"""
280
281    def __init__(
282        self,
283        element_validator: Validator[T],
284        min_length: int | None = None,
285        max_length: int | None = None
286    ):
287        self.element_validator = element_validator
288        self.min_length = min_length
289        self.max_length = max_length
290
291    def validate(self, value: list[T]) -> ValidationResult[list[T]]:
292        result = ValidationResult(value=value)
293
294        # Check list length
295        if self.min_length is not None and len(value) < self.min_length:
296            result.add_error(f"List length {len(value)} < minimum {self.min_length}")
297        if self.max_length is not None and len(value) > self.max_length:
298            result.add_error(f"List length {len(value)} > maximum {self.max_length}")
299
300        # Validate each element
301        for i, item in enumerate(value):
302            sub_result = self.element_validator.validate(item)
303            for error in sub_result.errors:
304                result.add_error(f"[{i}] {error}")
305            for warning in sub_result.warnings:
306                result.add_warning(f"[{i}] {warning}")
307
308        return result
309
310# ===== Demo =====
311
312if __name__ == "__main__":
313    print("=== Generic Validator Demo ===\n")
314
315    # Example 1: Basic validators
316    print("1. Basic Validators")
317    print("-" * 40)
318
319    not_empty = NotEmptyValidator()
320    print(f"  NotEmpty(''): {not_empty.validate('')}")
321    print(f"  NotEmpty('hello'): {not_empty.validate('hello')}")
322
323    # Example 2: Path validator
324    print("\n2. Path Validator")
325    print("-" * 40)
326
327    path_validator = PathExistsValidator(must_be_file=True)
328    result = path_validator.validate(Path("/etc/hosts"))
329    print(f"  /etc/hosts: valid={result.is_valid}")
330
331    result = path_validator.validate(Path("/nonexistent"))
332    print(f"  /nonexistent: valid={result.is_valid}, errors={result.errors}")
333
334    # Example 3: Composed validators
335    print("\n3. Composed Validators (AND)")
336    print("-" * 40)
337
338    username_validator = AndValidator[str]([
339        NotEmptyValidator(),
340        LengthValidator(min_length=3, max_length=20),
341        PatternValidator(r"^[a-z][a-z0-9_]*$", "Must be lowercase alphanumeric"),
342    ])
343
344    test_usernames = ["", "ab", "valid_user", "Invalid", "a" * 25]
345    for username in test_usernames:
346        result = username_validator.validate(username)
347        status = "PASS" if result.is_valid else "FAIL"
348        print(f"  '{username}': {status}")
349        if not result.is_valid:
350            for error in result.errors:
351                print(f"    - {error}")
352
353    # Example 4: Builder pattern
354    print("\n4. Builder Pattern")
355    print("-" * 40)
356
357    email_validator = (
358        validator_for(str)
359        .add(NotEmptyValidator())
360        .add(PatternValidator(
361            r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
362            "Invalid email format"
363        ))
364        .build()
365    )
366
367    test_emails = ["", "invalid", "test@example.com"]
368    for email in test_emails:
369        result = email_validator.validate(email)
370        status = "PASS" if result.is_valid else "FAIL"
371        print(f"  '{email}': {status}")
372
373    # Example 5: List validator
374    print("\n5. List Validator")
375    print("-" * 40)
376
377    tags_validator = ListValidator(
378        element_validator=AndValidator[str]([
379            NotEmptyValidator(),
380            LengthValidator(max_length=20),
381        ]),
382        min_length=1,
383        max_length=5
384    )
385
386    test_tags = [
387        ["python", "typing"],
388        [],
389        ["valid", "", "also-valid"],
390    ]
391
392    for tags in test_tags:
393        result = tags_validator.validate(tags)
394        status = "PASS" if result.is_valid else "FAIL"
395        print(f"  {tags}: {status}")
396        if not result.is_valid:
397            for error in result.errors:
398                print(f"    - {error}")
399
400    print("\n=== Demo Complete ===")

使用範例

基本使用

 1from pathlib import Path
 2
 3# Create validators
 4not_empty = NotEmptyValidator()
 5path_exists = PathExistsValidator(must_be_file=True)
 6
 7# Validate string
 8result = not_empty.validate("hello")
 9print(result.is_valid)  # True
10
11result = not_empty.validate("")
12print(result.is_valid)  # False
13print(result.errors)    # ["Value cannot be empty"]
14
15# Validate path
16result = path_exists.validate(Path("/etc/hosts"))
17print(result.is_valid)  # True (on Unix systems)
18
19# Using ValidationResult in boolean context
20if not_empty.validate("test"):
21    print("Validation passed!")

組合驗證

 1# Username validator: non-empty, 3-20 chars, lowercase alphanumeric
 2username_validator = AndValidator[str]([
 3    NotEmptyValidator(),
 4    LengthValidator(min_length=3, max_length=20),
 5    PatternValidator(r"^[a-z][a-z0-9_]*$"),
 6])
 7
 8# Test cases
 9result = username_validator.validate("valid_user")
10print(result.is_valid)  # True
11
12result = username_validator.validate("ab")
13print(result.is_valid)  # False
14print(result.errors)    # ["Length 2 < minimum 3"]
15
16# OR validation: accept either email or username format
17login_validator = OrValidator[str]([
18    PatternValidator(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"),
19    PatternValidator(r"^[a-z][a-z0-9_]{2,19}$"),
20])
21
22print(login_validator.validate("user@example.com").is_valid)  # True
23print(login_validator.validate("valid_user").is_valid)        # True
24print(login_validator.validate("X").is_valid)                  # False

使用 Builder 模式

 1# Fluent API for building validators
 2password_validator = (
 3    validator_for(str)
 4    .add(NotEmptyValidator())
 5    .add(LengthValidator(min_length=8, max_length=128))
 6    .add(PatternValidator(r".*[A-Z].*", "Must contain uppercase"))
 7    .add(PatternValidator(r".*[a-z].*", "Must contain lowercase"))
 8    .add(PatternValidator(r".*[0-9].*", "Must contain digit"))
 9    .build()
10)
11
12result = password_validator.validate("weakpass")
13print(result.errors)
14# ["Must contain uppercase", "Must contain digit"]
15
16result = password_validator.validate("Strong1Password")
17print(result.is_valid)  # True

驗證列表元素

 1# Validate a list of tags
 2tag_validator = NotEmptyValidator()
 3tags_validator = ListValidator(
 4    element_validator=tag_validator,
 5    min_length=1,
 6    max_length=10
 7)
 8
 9result = tags_validator.validate(["python", "typing", "generic"])
10print(result.is_valid)  # True
11
12result = tags_validator.validate(["valid", "", "also-valid"])
13print(result.is_valid)  # False
14print(result.errors)    # ["[1] Value cannot be empty"]

設計權衡

面向具體類型泛型設計
重用性低:每個類型需要獨立實作高:一次實作,多處使用
型別推導簡單:型別固定需要技巧:需正確標註 TypeVar
學習曲線低:直覺易懂中:需理解 Generic、Protocol
IDE 支援完整:型別明確需要正確標註才能獲得完整支援
執行效能略佳:無泛型開銷略差:有 Protocol 檢查開銷
錯誤訊息清晰:直接指出問題可能較模糊:泛型相關錯誤不易讀

何時選擇泛型設計?

選擇泛型設計當:

  • 驗證邏輯會用於多種類型
  • 需要組合多個驗證器
  • 正在建立可重用的驗證函式庫
  • 重視編譯時期的型別安全

選擇具體類型當:

  • 只驗證單一特定類型
  • 驗證邏輯非常簡單
  • 團隊對泛型不熟悉
  • 效能是關鍵考量

什麼時候該用這個技術?

適合使用

  • 需要驗證多種類型的函式庫
  • 驗證邏輯需要組合重用
  • 重視型別安全
  • API 設計需要表達「這個驗證器接受 T 類型」

不建議使用

  • 只驗證單一類型
  • 驗證邏輯很簡單(幾行 if-else 就能搞定)
  • 團隊不熟悉泛型語法
  • 程式碼不會被重用

練習

基礎練習

1. 實作 RangeValidator[int]LengthValidator[str]

參考上面的 RangeValidator 實作,確保它可以正確驗證整數範圍。 測試案例:

1age_validator = RangeValidator(min_value=0, max_value=150)
2assert age_validator.validate(25).is_valid
3assert not age_validator.validate(-1).is_valid
4assert not age_validator.validate(200).is_valid

2. 實作 EmailValidator

建立一個 Email 驗證器,組合 NotEmptyValidatorPatternValidator

1email_validator = EmailValidator()
2assert email_validator.validate("user@example.com").is_valid
3assert not email_validator.validate("invalid").is_valid

進階練習

1. 實作 ListValidator[T] 驗證列表中的每個元素

建立一個泛型列表驗證器,可以:

  • 驗證列表長度
  • 對每個元素執行子驗證器
  • 收集所有錯誤,標註元素索引
1int_list_validator = ListValidator(
2    element_validator=RangeValidator(min_value=0),
3    min_length=1
4)
5result = int_list_validator.validate([1, 2, -3, 4])
6# errors: ["[2] Value -3 < minimum 0"]

2. 實作 ConditionalValidator[T]

建立一個條件驗證器,只在條件成立時執行驗證:

1# Only validate age if it's provided (not None)
2optional_age_validator = ConditionalValidator(
3    condition=lambda x: x is not None,
4    validator=RangeValidator(min_value=0, max_value=150)
5)

挑戰題

1. 實作 SchemaValidator 驗證字典結構

建立一個驗證器,可以驗證字典的結構和值:

 1user_schema = SchemaValidator({
 2    "name": NotEmptyValidator(),
 3    "age": RangeValidator(min_value=0, max_value=150),
 4    "email": EmailValidator(),
 5}, required_keys=["name", "email"])
 6
 7result = user_schema.validate({
 8    "name": "Alice",
 9    "age": 30,
10    "email": "alice@example.com"
11})
12assert result.is_valid
13
14result = user_schema.validate({
15    "name": "",
16    "age": -5,
17})
18# errors: ["name: Value cannot be empty", "age: Value -5 < minimum 0", "Missing required key: email"]

提示:

  • 使用 dict[str, Validator[Any]] 作為 schema 類型
  • 處理可選欄位和必填欄位
  • 考慮巢狀 schema 的支援

延伸閱讀


上一章:異常設計架構 返回:模組 3.5:進階設計模式