Nỗi ám ảnh mang tên ‘Copy-Paste’ logic bổ trợ
Năm 2022, mình từng quản lý hệ thống monitoring với khoảng 45 script automation khác nhau. Các script này kiểm tra sức khỏe (health check) cho mọi thứ, từ Database, Redis đến Load Balancer. Khi đó, yêu cầu đặt ra là phải đo lường hiệu năng: mọi hàm chạy đều phải ghi log thời gian bắt đầu, kết thúc và tổng thời gian thực thi.
Lúc mới vào nghề, mình chọn cách thủ công nhất. Mình nhảy vào từng hàm, chèn thêm time.time() vào đầu và cuối. Kết quả là 45 đoạn code giống hệt nhau xuất hiện ở khắp nơi:
import time
def check_database_health():
start = time.time()
# Logic check DB phức tạp
print("Checking Database...")
time.sleep(1)
end = time.time()
print(f"Execution time: {end - start}s")
def check_redis_health():
start = time.time()
# Logic check Redis
print("Checking Redis...")
time.sleep(0.5)
end = time.time()
print(f"Execution time: {end - start}s")
Mọi thứ trở nên tồi tệ khi yêu cầu thay đổi. Thay vì in ra console, mình phải chuyển sang ghi log định dạng JSON vào file. Việc lục lại 45 hàm để sửa đổi khiến mình mất cả buổi chiều và cực kỳ dễ sai sót. Đây chính là lúc mình nhận ra mình đang vi phạm nguyên tắc DRY (Don’t Repeat Yourself) một cách nghiêm trọng.
Cái bẫy của việc trộn lẫn logic
Vấn đề không chỉ là tốn công. Khi bạn trộn logic nghiệp vụ (business logic) với logic bổ trợ như logging hay auth, code sẽ rất khó đọc. Đồng nghiệp vào xem sẽ phải “bơi” qua mớ hỗn độn tính toán thời gian mới thấy được mục đích thực sự của hàm.
Trong kỹ thuật, đây gọi là Cross-cutting Concerns. Đó là những tính năng xuất hiện ở nhiều tầng ứng dụng khác nhau. Nếu bạn thấy mình đang lặp lại cùng một cấu trúc code ở đầu và cuối nhiều hàm, đó là dấu hiệu Decorator cần được xuất hiện.
Giải pháp: Từ Wrapper thủ công đến Decorator chuyên nghiệp
1. Sử dụng Wrapper Function
Thay vì sửa trực tiếp, mình tách logic tính thời gian ra một hàm bao ngoài (wrapper). Cách này giúp tách biệt phần nào trách nhiệm của code:
def timer_wrapper(func):
def wrapper():
start = time.time()
func()
end = time.time()
print(f"Time taken: {end - start}s")
return wrapper
def my_task():
print("Doing something...")
# Sử dụng
wrapped_task = timer_wrapper(my_task)
wrapped_task()
Dù đã tách được logic, nhưng cách gọi wrapped_task() vẫn khá rườm rà. Nếu có hàng trăm hàm, việc gán biến thủ công như thế này sẽ là một thảm họa quản lý.
2. Làm chủ cú pháp @Decorator
Python cung cấp cú pháp @ để giải quyết vấn đề trên một cách thanh lịch. Decorator thực chất là một hàm nhận một hàm khác làm đầu vào và trả về một phiên bản “nâng cấp” của hàm đó. Nó giúp code sạch hơn, giữ nguyên tên hàm gốc và cực kỳ dễ bảo trì.
Triển khai Decorator chuẩn Senior
Để Decorator hoạt động linh hoạt cho mọi loại hàm (có tham số hoặc không), bạn nên sử dụng *args và **kwargs. Dưới đây là cấu trúc mình luôn sử dụng trong các dự án thực tế:
import time
from functools import wraps
def logger_decorator(func):
@wraps(func) # Giữ lại metadata (tên hàm, docstring) gốc
def wrapper(*args, **kwargs):
print(f"--- Khởi chạy: {func.__name__} ---")
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"--- Hoàn thành: {func.__name__} sau {end_time - start_time:.4f}s ---")
return result
return wrapper
@logger_decorator
def sync_data(source, destination):
"""Mô phỏng đồng bộ dữ liệu giữa các server"""
print(f"Đang chuyển dữ liệu từ {source} sang {destination}...")
time.sleep(2)
return "Success"
Lưu ý quan trọng: Luôn dùng @wraps(func). Nếu thiếu nó, thuộc tính sync_data.__name__ sẽ bị đổi thành 'wrapper'. Điều này khiến việc debug hoặc làm tài liệu tự động trở nên cực kỳ khó khăn.
3 ứng dụng thực tế trong DevOps và Backend
Decorator không chỉ để log thời gian. Trong thực tế, mình thường áp dụng cho 3 kịch bản sau:
- Retry Logic: Tự động gọi lại API 3-5 lần nếu gặp lỗi mạng trước khi báo thất bại.
- Phân quyền (RBAC): Kiểm tra quyền Admin trước khi thực thi các lệnh nhạy cảm như xóa database.
- Caching: Lưu kết quả các query nặng vào Redis, giúp giảm tải cho DB tới 70% trong các task lặp lại.
Ví dụ về Decorator kiểm soát quyền truy cập:
def require_admin(func):
@wraps(func)
def wrapper(user_role, *args, **kwargs):
if user_role != "admin":
print("Từ chối: Bạn không có quyền thực hiện hành động này!")
return None
return func(user_role, *args, **kwargs)
return wrapper
@require_admin
def delete_system_logs(user_role):
print("Đang xóa log hệ thống...")
Lời kết
Decorator không phải là kỹ thuật quá cao siêu. Nó là một cách tư duy khác về tổ chức code. Thay vì viết code theo chiều dọc (thêm dòng lệnh vào trong hàm), chúng ta bọc nó theo các lớp (layers).
Nếu bạn thấy mình đang viết đi viết lại một đoạn logic bổ trợ ở quá 3 hàm khác nhau, hãy dừng lại 5 phút. Chuyển nó thành Decorator sẽ giúp bạn tiết kiệm hàng giờ bảo trì sau này. Code của bạn sẽ chuyên nghiệp, dễ đọc và quan trọng nhất là dễ mở rộng.

