Strategy pattern trong C++: đổi cách xử lý ngay lúc chương trình chạy

Strategy pattern trong C++: đổi cách xử lý ngay lúc chương trình chạy - Khuê Nguyễn Creator

Trong bài này chúng ta sẽ học Strategy pattern trong C++: tách mỗi cách xử lý thành một “chiến lược” riêng, để đối tượng có thể đổi chiến lược ngay lúc chương trình đang chạy. Bài trình bày 2 cách viết: cách cổ điển bằng interface và hàm ảo, và cách gọn hơn bằng std::function với lambda.

Bài 33 trong Serie Lập trình C++ từ A tới Z

Bài toán: mỗi loại độc giả một cách tính phạt

Thư viện có 3 loại độc giả, mỗi loại tính tiền phạt trả sách muộn khác nhau:

  • Sinh viên: 2000 đồng mỗi ngày quá hạn.
  • Giảng viên: miễn phạt 7 ngày đầu, từ ngày thứ 8 là 1000 đồng mỗi ngày.
  • Khách vãng lai: 10000 đồng phí cố định cộng 5000 đồng mỗi ngày.

Cách ngây thơ là một hàm với chuỗi if (loai == "sinh vien") ... else if (loai == "giang vien") .... Thêm loại độc giả mới phải sửa hàm đó, và nếu nhiều chỗ khác cũng phân nhánh theo loại độc giả thì phải sửa tất cả. Thêm nữa, một sinh viên tốt nghiệp và trở thành giảng viên thì cách tính của họ phải đổi.

Strategy pattern là gì?

Strategy (chiến lược) tách mỗi cách xử lý thành một class riêng cùng thực thi một interface. Đối tượng cần xử lý (ở đây là phiếu mượn) giữ một chiến lược và giao việc cho nó, không tự quyết định cách tính.

Giống việc đi từ nhà tới trường: đi xe máy, đi xe buýt hay đi bộ là 3 chiến lược khác nhau, cùng một mục tiêu. Hôm nào trời mưa thì đổi sang xe buýt, người đi không phải “thay đổi bản thân”, chỉ đổi phương tiện.

Sơ đồ Strategy pattern trong C++: PhieuMuon giữ interface CachTinhPhat với 3 chiến lược sinh viên, giảng viên, khách

Cách viết Strategy pattern trong C++ bằng interface

#include <iostream>
#include <memory>
#include <string>

// Chien luoc tinh tien phat tra sach muon
class CachTinhPhat
{
public:
    virtual ~CachTinhPhat() = default;
    virtual int tinh(int soNgayQuaHan) const = 0;
};

class PhatSinhVien : public CachTinhPhat
{
public:
    int tinh(int ngay) const override { return ngay * 2000; }
};

class PhatGiangVien : public CachTinhPhat
{
public:
    int tinh(int ngay) const override { return ngay > 7 ? (ngay - 7) * 1000 : 0; }   // 7 ngay dau mien phat
};

class PhatKhachNgoai : public CachTinhPhat
{
public:
    int tinh(int ngay) const override { return 10000 + ngay * 5000; }
};

class PhieuMuon
{
public:
    PhieuMuon(const std::string& docGia, std::unique_ptr<CachTinhPhat> cach)
        : docGia_(docGia), cachPhat_(std::move(cach)) {}

    void doiCachPhat(std::unique_ptr<CachTinhPhat> cach) { cachPhat_ = std::move(cach); }

    void inTienPhat(int soNgayQuaHan) const
    {
        std::cout << docGia_ << " qua han " << soNgayQuaHan << " ngay: "
                  << cachPhat_->tinh(soNgayQuaHan) << " dong\n";
    }

private:
    std::string docGia_;
    std::unique_ptr<CachTinhPhat> cachPhat_;
};

int main()
{
    PhieuMuon an("An (sinh vien)", std::make_unique<PhatSinhVien>());
    PhieuMuon lan("Co Lan (giang vien)", std::make_unique<PhatGiangVien>());
    PhieuMuon khach("Khach vang lai", std::make_unique<PhatKhachNgoai>());

    an.inTienPhat(10);
    lan.inTienPhat(10);
    khach.inTienPhat(10);

    an.doiCachPhat(std::make_unique<PhatGiangVien>());     // An tot nghiep, chuyen thanh giang vien
    an.inTienPhat(10);
    return 0;
}

Ba cách tính tiền phạt cho 10 ngày quá hạn và đổi chiến lược của An lúc chạy

  • CachTinhPhat là interface với một hàm thuần ảo tinh(), như bài tính trừu tượng trong C++.
  • Mỗi cách tính là một class nhỏ, chỉ làm đúng một việc và dễ kiểm thử riêng.
  • PhieuMuon giữ chiến lược qua std::unique_ptr và gọi cachPhat_->tinh(...) mà không biết đang là cách nào.
  • doiCachPhat thay chiến lược lúc chạy: cùng một phiếu của An, sau khi đổi thì tiền phạt 10 ngày giảm từ 20000 còn 3000 đồng.

Thêm loại độc giả mới, ví dụ “nhân viên thư viện”, chỉ cần viết thêm một class kế thừa CachTinhPhat. PhieuMuon không phải sửa gì.

Cách viết gọn bằng std::function và lambda

Khi chiến lược chỉ là một hàm không có dữ liệu riêng, viết cả một class cho mỗi chiến lược hơi dài. C++ hiện đại cho phép dùng std::function như bài Observer. Ví dụ đổi tiêu chí sắp xếp kho sách:

#include <algorithm>
#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <vector>

struct Sach
{
    std::string ten;
    int nam;
    int luotMuon;
};

using CachSapXep = std::function<bool(const Sach&, const Sach&)>;

int main()
{
    std::vector<Sach> kho = {{"Lap trinh C++", 2020, 35}, {"Clean Code", 2008, 52}, {"Python co ban", 2021, 40}};

    std::map<std::string, CachSapXep> cacCach = {
        {"ten", [](const Sach& a, const Sach& b) { return a.ten < b.ten; }},
        {"nam", [](const Sach& a, const Sach& b) { return a.nam > b.nam; }},
        {"luot", [](const Sach& a, const Sach& b) { return a.luotMuon > b.luotMuon; }},
    };

    for (const char* tieuChi : {"ten", "nam", "luot"})
    {
        std::sort(kho.begin(), kho.end(), cacCach[tieuChi]);
        std::cout << "Sap theo " << tieuChi << ":";
        for (const Sach& s : kho)
            std::cout << " [" << s.ten << "]";
        std::cout << '\n';
    }
    return 0;
}

Strategy bằng lambda lưu trong std::map, sắp xếp kho sách theo tên, năm và lượt mượn

Mỗi lambda là một chiến lược sắp xếp, lưu trong std::map theo tên tiêu chí. Người dùng chọn “sắp theo năm” trên giao diện thì chương trình tra map lấy đúng chiến lược, không cần chuỗi if. Chính std::sort nhận hàm so sánh làm tham số cũng là một dạng Strategy có sẵn trong thư viện chuẩn.

Interface + hàm ảo std::function + lambda
Chiến lược có dữ liệu, trạng thái riêng Phù hợp Được, nhưng kém rõ ràng
Chiến lược chỉ là một phép tính ngắn Dài dòng Gọn
Có nhiều hàm đi kèm nhau (tính + mô tả + kiểm tra) Phù hợp Phải lưu nhiều function
Kiểm thử, tái sử dụng ở nhiều nơi Dễ, mỗi chiến lược là một class có tên Phụ thuộc chỗ khai báo lambda

Lưu ý: Strategy và Factory hay đi chung. Chỗ đọc loại độc giả từ file hoặc từ giao diện nên dùng một hàm factory trả về đúng std::unique_ptr<CachTinhPhat>, để danh sách các chiến lược chỉ xuất hiện ở một chỗ, không rải khắp chương trình.

Strategy khác State thế nào?

Hai pattern trông giống nhau về cấu trúc: một đối tượng giữ con trỏ tới interface và giao việc. Khác biệt nằm ở ai đổi và vì sao đổi:

  • Strategy: bên ngoài chọn chiến lược, thường đổi ít, các chiến lược không biết nhau. An đổi cách tính phạt vì thư viện cập nhật thông tin.
  • State: đối tượng tự chuyển trạng thái theo sự kiện, và chính trạng thái hiện tại quyết định trạng thái tiếp theo. Cuốn sách tự chuyển từ “đang mượn” sang “quá hạn” khi hết hạn.

Phần State là nội dung của bài tiếp theo.

Bài tập

  1. Thêm chiến lược PhatNhanVien: nhân viên thư viện không bao giờ bị phạt. Tạo phiếu mượn cho một nhân viên và in tiền phạt.
  2. Viết hàm factory std::unique_ptr<CachTinhPhat> taoCachPhat(const std::string& loai) nhận “sinhvien”, “giangvien”, “khach” và trả về chiến lược tương ứng.
  3. Viết lại ví dụ tính phạt dùng std::function<int(int)> thay cho interface. So sánh 2 cách.
  4. Thêm tiêu chí sắp xếp “tên tác giả” vào ví dụ lambda, chỉ thêm một dòng vào map.

Lời giải sẽ có trong bài tập tổng hợp của chương nhé.

Kết

Strategy pattern trong C++ tách mỗi cách xử lý thành một chiến lược riêng, đối tượng chỉ giữ và giao việc cho chiến lược, nên thêm cách xử lý mới hoặc đổi cách xử lý lúc chạy không phải sửa code cũ. Dùng interface khi chiến lược có dữ liệu và nhiều hàm đi kèm, dùng std::function với lambda khi chiến lược chỉ là một phép tính ngắn.

Tham khảo thêm ở Refactoring.Guru: Strategy. Bài tiếp theo chúng ta học State pattern và máy trạng thái, pattern cuối cùng của chương. Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè nhé.

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *