Tối ưu hóa mã nguồn với Strategy Pattern trong Python

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
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.

Thẻ: strategy pattern python design patterns solid First-Class Functions

Đăng vào ngày 3 tháng 9 lúc 10:15