Visitor
"Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates." — GoF, Design Patterns (1994)
Có bao giờ bạn nhìn vào một cấu trúc object và nghĩ: "Trời ơi, mình muốn thêm một thao tác mới mà không muốn sửa từng class một!"? Visitor là câu trả lời cho lời cầu nguyện đó.
Visitor là một behavioral pattern cho phép tách các thao tác (operations) khỏi object hierarchy. Pattern này đặc biệt hữu ích khi bạn có một cấu trúc object ổn định (ít thay đổi) nhưng lại cần thêm nhiều thao tác mới lên cấu trúc đó. Nói như một ông già từng trải: "Nếu cái cây không thể đến gặp nhà tiều phu, hãy để nhà tiều phu đến gặp cái cây."
Bài toán chi tiết
Tôi từng làm cho một công ty phần mềm — và một trong những dự án ác mộng nhất là xây dựng hệ thống phân tích mã nguồn tĩnh (Static Code Analysis). Hãy tưởng tượng bạn phải đọc và phân tích cây cú pháp trừu tượng (AST — Abstract Syntax Tree) của code Python để thực hiện đủ thứ việc:
Cấu trúc AST (object hierarchy) ổn định gồm các node:
| Node | Ý nghĩa | Thuộc tính |
|---|---|---|
NumberNode | Số nguyên/thực | value: int |
StringNode | Chuỗi | value: str |
BinaryOpNode | Phép toán hai ngôi | left: Node, right: Node, op: str |
VariableNode | Biến | name: str |
AssignmentNode | Gán giá trị | target: str, value: Node |
FunctionCallNode | Gọi hàm | name: str, args: List[Node] |
IfNode | Câu lệnh if | condition: Node, then_body: List[Node], else_body: List[Node] |
Các thao tác (operations) muốn thêm lên AST:
- Code Generator: Sinh code Python, JavaScript, hoặc bytecode từ AST
- Type Checker: Kiểm tra kiểu dữ liệu, phát hiện lỗi type mismatch
- Optimizer: Tối ưu hóa (constant folding, dead code elimination)
- Formatter: Format code theo chuẩn PEP 8
- Complexity Analyzer: Tính độ phức tạp cyclomatic
- Security Auditor: Phát hiện lỗ hổng bảo mật (SQL injection, XSS)
Cách tiếp cận ngây thơ — tôi dám cá nhiều bạn sẽ nghĩ đến đầu tiên — là thêm method vào mỗi node class:
class NumberNode:
def generate_code(self): ...
def check_type(self): ...
def optimize(self): ...
def format(self): ...
def complexity(self): ...
def security_audit(self): ...
class BinaryOpNode:
def generate_code(self): ...
def check_type(self): ...
def optimize(self): ...
# ... cứ thế cho mỗi thao tác mới
Vấn đề — nói thật, đây là cơn ác mộng bảo trì:
- Vi phạm Single Responsibility: Mỗi node class phải lo code gen, type check, optimize, format, security... Một class mà làm việc của năm class
- Vi phạm Open/Closed Principle: Thêm thao tác mới? Sửa TẤT CẢ node class. Một cơn ác mộng.
- Class phình to:
BinaryOpNode500+ dòng. Với 10 thao tác, bạn có thể hình dung. - Logic phân tán: Cùng một thao tác code generation mà bị phân mảnh khắp các class. Muốn sửa? Mất cả buổi để tìm.
- Khó maintain: Thêm ngôn ngữ target mới (ví dụ: Java) = đi sửa từng node một. Không có tương lai.
Giải pháp với Pattern
Visitor pattern giải quyết vấn đề này bằng cơ chế double dispatch — nghe cao siêu nhưng thực ra rất đơn giản:
- Element (
ASTNode): Interface vớiaccept(visitor)method — mỗi element gọi đúngvisitor.visit_XXX(self) - Visitor (
ASTVisitor): Interface vớivisit_XXX(node)method cho mỗi loại element - ConcreteVisitor (
CodeGenerator,TypeChecker, ...): Implement các thao tác cụ thể - ObjectStructure (AST tree): Collection các element, iterate và apply visitor
Điều kỳ diệu: Khi cần thêm thao tác mới, bạn chỉ cần thêm một ConcreteVisitor. Không sửa bất kỳ node class nào. Tôi nhớ lần đầu áp dụng Visitor, tôi đã thốt lên: "Trời, tại sao không ai nói cho tôi biết sớm hơn?"