Design Pattern: STRATEGY

Mẫu thiết kế Strategy (Chiến lược)

Strategy là một mẫu thiết kế hành vi (Behavioral Design Pattern) cho phép bạn xác định một tập hợp các thuật toán, đặt mỗi thuật toán vào một lớp riêng biệt và làm cho các đối tượng này có thể thay thế lẫn nhau.


Vấn đề

Một ngày nọ, bạn quyết định tạo một ứng dụng điều hướng dành cho khách du lịch. Ứng dụng tập trung vào một bản đồ đẹp mắt, giúp người dùng dễ dàng định hướng trong bất kỳ thành phố nào.

Một trong những tính năng được yêu cầu nhiều nhất là lập kế hoạch lộ trình tự động. Người dùng chỉ cần nhập một địa chỉ và ứng dụng sẽ hiển thị tuyến đường nhanh nhất đến đích trên bản đồ.

  • Phiên bản đầu tiên của ứng dụng chỉ hỗ trợ tuyến đường ô tô 🚗. Điều này khiến những người lái xe rất vui mừng.
  • Nhưng không phải ai cũng thích lái xe khi đi du lịch! Vì vậy, bản cập nhật tiếp theo đã thêm tuyến đường đi bộ 🚶‍♂️.
  • Ngay sau đó, bạn tiếp tục thêm tuyến đường sử dụng phương tiện công cộng 🚌.

Tuy nhiên, đây mới chỉ là khởi đầu! Bạn dự định:

✔️ Thêm lộ trình cho người đi xe đạp 🚴‍♂️.

✔️ Tạo lộ trình tham quan các điểm du lịch 🏛️.

Ứng dụng ngày càng có nhiều thuật toán lập lộ trình khác nhau. Nếu tiếp tục mở rộng bằng cách thêm nhiều điều kiện (if-else, switch-case) vào mã nguồn chính, thì việc bảo trì và mở rộng ứng dụng sẽ trở nên rất khó khăn.

💡 Giải pháp? → Áp dụng mẫu thiết kế Strategy! 🚀

Vấn đề kỹ thuật

Mặc dù từ góc độ kinh doanh, ứng dụng của bạn đã thành công, nhưng về mặt kỹ thuật, nó lại khiến bạn đau đầu.

  • Mỗi lần thêm một thuật toán định tuyến mới, kích thước của lớp điều hướng lại tăng gấp đôi.
  • Đến một thời điểm, mã nguồn trở nên quá cồng kềnh và khó bảo trì.
  • Bất kỳ thay đổi nào (sửa lỗi nhỏ, điều chỉnh thuật toán…) đều có thể ảnh hưởng đến toàn bộ lớp, làm tăng nguy cơ phát sinh lỗi trong mã vốn đang hoạt động tốt.

Ngoài ra, khi công ty phát triển, bạn có thêm đồng đội 👥. Nhưng họ lại phàn nàn rằng:

  • Họ tốn quá nhiều thời gian để giải quyết xung đột khi gộp mã (merge conflicts).
  • Mỗi lần thêm tính năng mới đều phải chỉnh sửa một lớp khổng lồ, gây xung đột với phần mã của người khác.

💡 Giải pháp?Áp dụng mẫu thiết kế Strategy! 🚀


Giải pháp bằng Strategy Pattern

Mẫu thiết kế Strategy đề xuất rằng bạn trích xuất tất cả các thuật toán khác nhau ra khỏi lớp chính, đưa chúng vào các lớp riêng biệt, gọi là chiến lược (strategies).

  • Lớp chính, gọi là context, sẽ chứa một tham chiếu đến một strategy.
  • Thay vì tự thực thi thuật toán, context ủy quyền công việc đó cho strategy liên kết với nó.
  • Context không chọn thuật toán. Thay vào đó, client sẽ truyền strategy mong muốn vào context.

💡 Kết quả?

✔️ Dễ mở rộng – Bạn có thể thêm hoặc sửa đổi thuật toán mà không ảnh hưởng đến context hoặc các strategies khác.

✔️ Code gọn gàng – Không còn những khối if-else khổng lồ trong lớp chính.

✔️ Hỗ trợ teamwork tốt hơn – Mỗi người có thể làm việc trên một strategy riêng mà không lo xung đột mã. 🚀

Trong ứng dụng điều hướng của chúng ta, mỗi thuật toán định tuyến có thể được tách ra thành một lớp riêng, với một phương thức duy nhất là buildRoute.

  • Phương thức này sẽ nhận vào điểm xuất phátđích đến, sau đó trả về danh sách các điểm kiểm tra (checkpoints) trên tuyến đường.
  • Dù có cùng một điểm bắt đầu và kết thúc, mỗi lớp định tuyến có thể tạo ra một tuyến đường khác nhau.
  • Tuy nhiên, lớp điều hướng chính (navigator) không quan tâm đến thuật toán nào được chọn, vì nhiệm vụ chính của nó chỉ là hiển thị các điểm kiểm tra trên bản đồ.

Lớp này có một phương thức để thay đổi chiến lược định tuyến hiện tại, giúp các thành phần khác trong giao diện (như các nút trên UI) có thể thay đổi thuật toán định tuyến một cách linh hoạt. 🚀

Hãy tưởng tượng rằng bạn cần đến sân bay. Bạn có thể bắt xe buýt, gọi taxi hoặc đạp xe.

Đây chính là các chiến lược di chuyển của bạn. Bạn có thể chọn một chiến lược phù hợp tùy vào các yếu tố như ngân sách hoặc thời gian. 🚖🚲🚌

Mô tả chung

  1. Context duy trì một tham chiếu đến một trong các chiến lược cụ thể và chỉ giao tiếp với đối tượng này thông qua giao diện chiến lược (Strategy Interface).
  2. Giao diện Strategy chung cho tất cả các chiến lược cụ thể. Nó khai báo một phương thức mà Context sử dụng để thực thi chiến lược.
  3. Chiến lược cụ thể (Concrete Strategies) triển khai các biến thể khác nhau của thuật toán mà Context sử dụng.
  4. Context gọi phương thức thực thi trên đối tượng chiến lược đã liên kết mỗi khi cần chạy thuật toán. Context không cần biết loại chiến lược nào đang hoạt động hoặc cách thuật toán được thực thi.
  5. Client tạo một đối tượng chiến lược cụ thể và truyền nó vào Context. Context cung cấp một phương thức setter cho phép Client thay đổi chiến lược được liên kết với Context trong thời gian chạy.

Ví dụ này minh họa cách Context sử dụng nhiều chiến lược để thực thi các phép toán số học khác nhau:

// Giao diện chiến lược khai báo các phương thức chung cho tất cả các thuật toán.
interface Strategy {
    method execute(a, b)
}

// Các chiến lược cụ thể triển khai thuật toán theo cách riêng của chúng.
class ConcreteStrategyAdd implements Strategy {
    method execute(a, b) {
        return a + b
    }
}

class ConcreteStrategySubtract implements Strategy {
    method execute(a, b) {
        return a - b
    }
}

class ConcreteStrategyMultiply implements Strategy {
    method execute(a, b) {
        return a * b
    }
}

// Context định nghĩa giao diện làm việc với Client.
class Context {
    private strategy: Strategy  // Tham chiếu đến chiến lược cụ thể.

    // Constructor hoặc setter để thay đổi chiến lược trong thời gian chạy.
    method setStrategy(Strategy strategy) {
        this.strategy = strategy
    }

    // Giao công việc thực thi thuật toán cho đối tượng chiến lược.
    method executeStrategy(int a, int b) {
        return strategy.execute(a, b)
    }
}

// Mã Client
class ExampleApplication {
    method main() {
        Create context object. // Tạo đối tượng Context.

        Read first number.
        Read last number.
        Read the desired action from user input.

        if (action == addition) {
            context.setStrategy(new ConcreteStrategyAdd())
        }

        if (action == subtraction) {
            context.setStrategy(new ConcreteStrategySubtract())
        }

        if (action == multiplication) {
            context.setStrategy(new ConcreteStrategyMultiply())
        }

        result = context.executeStrategy(firstNumber, secondNumber)

        Print result.
    }
}

Khi nào nên sử dụng Strategy Pattern?

  • Khi bạn cần thay đổi thuật toán mà một đối tượng sử dụng trong thời gian chạy.
  • Khi có nhiều lớp tương tự nhau, chỉ khác nhau về cách chúng thực hiện một hành vi cụ thể.
  • Khi bạn muốn tách rời logic nghiệp vụ của một lớp khỏi việc triển khai các thuật toán phức tạp.
  • Khi mã của bạn có quá nhiều câu lệnh điều kiện (if-else hoặc switch-case) để lựa chọn giữa các biến thể thuật toán khác nhau.

Ưu điểm & Nhược điểm

Ưu điểm

✅ Có thể thay đổi thuật toán tại runtime mà không cần sửa đổi mã nguồn của Context.

✅ Giúp tách rời logic thuật toán khỏi phần còn lại của chương trình.

✅ Thay thế kế thừa (Inheritance) bằng thành phần (Composition).

✅ Tuân theo Nguyên tắc Mở/Đóng (Open/Closed Principle) – có thể thêm chiến lược mới mà không cần thay đổi Context.

Nhược điểm

❌ Nếu chỉ có một vài thuật toán và chúng ít thay đổi, việc sử dụng Strategy có thể làm mã trở nên phức tạp không cần thiết.

❌ Client phải biết về các chiến lược để chọn cái phù hợp.

❌ Một số ngôn ngữ lập trình hỗ trợ kiểu dữ liệu hàm (Function Type), có thể giúp tránh việc tạo quá nhiều lớp không cần thiết.

Related Posts