Khi phát triển phần mềm, việc xử lý các luồng logic nghiệp vụ thường dẫn đến những khối lệnh điều kiện if/elif/else khổng lồ. Ví dụ, một hàm tính toán phí vận chuyển có thể được viết như sau:
def calculate_shipping_fee(method, weight):
if method == "standard":
return weight * 10
elif method == "express":
return weight * 25 + 50
elif method == "international":
return weight * 100 + 200
Trong giai đoạn đầu, cách viết này có vẻ ổn. Tuy nhiên, khi quy mô dự án mở rộng, hàm này sẽ liên tục phình to, vi phạm nguyên tắc Mở/Đóng (Open/Closed Principle) và khiến việc bảo trì, gỡ lỗi trở nên rủi ro. Bất kỳ thay đổi nào ở một nhánh logic cũng có thể vô tình phá vỡ các nhánh khác. Đây là lúc Strategy Pattern phát huy tác dụng.
Bản chất của Strategy Pattern
Strategy Pattern là một mẫu thiết kế thuộc nhóm Hành vi (Behavioral). Nó cho phép định nghĩa một tập hợp các thuật toán, đóng gói từng thuật toán lại và làm cho chúng có thể hoán đổi cho nhau một cách linh hoạt.
Mục tiêu chính:
- Đóng gói các thuật toán độc lập.
- Chuyển đổi hành vi ở thời điểm chạy (runtime) mà không cần sửa đổi mã nguồn gốc.
- Loại bỏ các cấu trúc rẽ nhánh phức tạp, giúp mã nguồn dễ đọc và dễ kiểm thử.
Cấu trúc cốt lõi
Mô hình này thường bao gồm ba thành phần:
- Strategy (Giao diện chung): Khai báo phương thức chung cho mọi thuật toán.
- ConcreteStrategy (Chiến lược cụ thể): Các lớp thực thi giao diện Strategy, mỗi lớp chứa một thuật toán riêng biệt.
- Context (Ngữ cảnh): Lớp sử dụng Strategy thông qua giao diện chung, hoàn toàn không phụ thuộc vào lớp cụ thể nào.
Triển khai Strategy Pattern (Hướng đối tượng)
Chuyển đổi bài toán tính phí vận chuyển ở trên sang áp dụng mẫu thiết kế chuẩn OOP trong Python.
1. Định nghĩa giao diện
Sử dụng module abc để tạo một abstract base class làm khuôn mẫu.
from abc import ABC, abstractmethod
class ShippingStrategy(ABC):
@abstractmethod
def calculate(self, weight: float) -> float:
pass
2. Xây dựng các thuật toán cụ thể
Mỗi phương thức vận chuyển sẽ là một class độc lập.
class StandardShipping(ShippingStrategy):
def calculate(self, weight: float) -> float:
return weight * 10.0
class ExpressShipping(ShippingStrategy):
def calculate(self, weight: float) -> float:
return (weight * 25.0) + 50.0
class InternationalShipping(ShippingStrategy):
def calculate(self, weight: float) -> float:
return (weight * 100.0) + 200.0
3. Tạo lớp Context
Lớp này sẽ nhận strategy từ bên ngoài và ủy quyền việc tính toán cho strategy đó.
class OrderProcessor:
def __init__(self, strategy: ShippingStrategy):
self._strategy = strategy
def set_strategy(self, strategy: ShippingStrategy):
self._strategy = strategy
def get_shipping_fee(self, weight: float) -> float:
return self._strategy.calculate(weight)
4. Sử dụng
cart_weight = 5.0
# Khởi tạo với giao hàng tiêu chuẩn
order = OrderProcessor(StandardShipping())
print(f"Phí giao tiêu chuẩn: {order.get_shipping_fee(cart_weight)}")
# Chuyển đổi sang giao hàng hỏa tốc ở runtime
order.set_strategy(ExpressShipping())
print(f"Phí giao hỏa tốc: {order.get_shipping_fee(cart_weight)}")
Cách tiếp cận Pythonic (Sử dụng First-class Functions)
Python coi hàm là các đối tượng hạng nhất (first-class citizens). Do đó, nếu thuật toán không yêu cầu duy trì trạng thái phức tạp, chúng ta không nhất thiết phải tạo ra một hệ thống các class. Việc truyền hàm trực tiếp sẽ giúp mã nguồn ngắn gọn và hiệu quả hơn.
def standard_shipping(weight: float) -> float:
return weight * 10.0
def express_shipping(weight: float) -> float:
return (weight * 25.0) + 50.0
class OrderProcessor:
def __init__(self, strategy_func):
self.strategy_func = strategy_func
def get_shipping_fee(self, weight: float) -> float:
return self.strategy_func(weight)
# Truyền trực tiếp hàm vào context
processor = OrderProcessor(standard_shipping)
print(processor.get_shipping_fee(5.0))
Cách tiếp cận này tận dụng tối đa đặc trưng của ngôn ngữ, giảm thiểu boilerplate code nhưng vẫn giữ nguyên tư tưởng của Strategy Pattern.
So sánh với các phương pháp khác
Strategy Pattern so với if/else truyền thống
| Tiêu chí | Lệnh if/else | Strategy Pattern |
|---|---|---|
| Tuân thủ Open/Closed | Không | Có |
| Khả năng mở rộng | Kém (phải sửa đổi code cũ) | Tốt (chỉ cần thêm class/hàm mới) |
| Cấu trúc mã nguồn | Tập trung, cồng kềnh | Phân tán, module hóa cao |
Phân biệt với Bridge Pattern
Mặc dù sơ đồ lớp có nét tương đồng, nhưng mục đích thiết kế hoàn toàn khác biệt:
| Tiêu chí | Strategy Pattern | Bridge Pattern |
|---|---|---|
| Mục đích | Thay đổi thuật toán/hành vi | Tách biệt phần trừu tượng và phần thực thi |
| Trọng tâm | Tính linh hoạt của logic | Cấu trúc đa chiều của hệ thống |
Ứng dụng thực tế
Mẫu thiết kế này xuất hiện ở hầu hết các hệ thống phức tạp:
- Hệ thống thanh toán: Chuyển đổi cổng thanh toán giữa Stripe, PayPal, MoMo.
- Xác thực người dùng: Lựa chọn cơ chế đăng nhập bằng mật khẩu, sinh trắc học, hoặc OAuth2.
- Xử lý dữ liệu: Lựa chọn thuật toán nén (ZIP, RAR, GZIP) hoặc thuật toán mã hóa.
- Hệ thống风控 (Risk Management): Áp dụng các bộ quy tắc chặn khác nhau dựa trên địa lý, tần suất hoặc danh sách đen.
Thời điểm áp dụng và các cạm bẫy
Nên sử dụng khi:
- Hệ thống có nhiều biến thể của một thuật toán và cần chuyển đổi linh hoạt.
- Cần che giấu logic phức tạp của thuật toán khỏi các thành phần khác.
- Một lớp chứa quá nhiều nhánh điều kiện liên quan đến hành vi.
Lưu ý tránh:
- Over-engineering (Thiết kế quá mức): Nếu chỉ có hai lựa chọn cố định và logic cực kỳ đơn giản, việc tạo ra các class riêng biệt sẽ làm hệ thống rườm rà không cần thiết.
- Class Explosion (Bùng nổ số lượng class): Khi có hàng chục chiến lược, số lượng file mã nguồn sẽ tăng vọt. Vấn đề này có thể được giải quyết bằng cách kết hợp với Factory Pattern để quản lý khởi tạo, hoặc sử dụng kỹ thuật ánh xạ (Dictionary mapping) đối với cách tiếp cận dùng hàm.