State pattern trong C++ và máy trạng thái: hành vi thay đổi theo trạng thái

State pattern trong C++ và máy trạng thái: hành vi thay đổi theo trạng thái - Khuê Nguyễn Creator

Trong bài này chúng ta sẽ học State pattern trong C++ và khái niệm máy trạng thái: một đối tượng phản ứng khác nhau với cùng một sự kiện tùy theo trạng thái hiện tại của nó. Đây là pattern cuối của chương design pattern, và cũng là khái niệm các bạn sẽ gặp rất nhiều trong lập trình nhúng: nút nhấn, giao thức truyền thông, điều khiển động cơ đều là máy trạng thái.

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

Máy trạng thái là gì?

Máy trạng thái (state machine) mô tả một đối tượng có số trạng thái hữu hạn, và các sự kiện làm nó chuyển từ trạng thái này sang trạng thái khác. Quan trọng là: cùng một sự kiện, ở trạng thái khác nhau thì kết quả khác nhau, và có những sự kiện không hợp lệ ở một số trạng thái.

Một cuốn sách trong thư viện có 3 trạng thái:

Sơ đồ máy trạng thái cuốn sách: có sẵn, đang mượn, quá hạn với các sự kiện mượn, trả, hết hạn

  • Có sẵn: mượn được. Trả thì vô nghĩa, vì sách đang nằm trên kệ.
  • Đang mượn: trả được, và hết hạn thì thành quá hạn. Không ai mượn thêm được.
  • Quá hạn: trả được nhưng phải thu tiền phạt.

Ví dụ đời thường quen thuộc là đèn giao thông: xanh, vàng, đỏ, mỗi trạng thái chỉ chuyển sang đúng một trạng thái tiếp theo. Hay chiếc chuột máy tính phân biệt click, double click và giữ chuột như bài STM32 nút nhấn 3 chế độ.

Máy trạng thái bằng switch và vấn đề của nó

Cách viết phổ biến nhất là một biến enum lưu trạng thái và các lệnh if, switch xử lý sự kiện:

#include <iostream>
#include <string>

enum class TrangThai { CoSan, DangMuon, QuaHan };

std::string ten(TrangThai t)
{
    switch (t)
    {
    case TrangThai::CoSan:    return "Co san";
    case TrangThai::DangMuon: return "Dang muon";
    case TrangThai::QuaHan:   return "Qua han";
    }
    return "?";
}

struct Sach
{
    TrangThai trangThai = TrangThai::CoSan;

    void xuLy(const std::string& suKien)
    {
        if (suKien == "muon")
            trangThai = TrangThai::DangMuon;
        else if (suKien == "tra")
            trangThai = TrangThai::CoSan;
        else if (suKien == "het han")
            trangThai = TrangThai::QuaHan;
        std::cout << "  " << suKien << " -> " << ten(trangThai) << '\n';
    }
};

int main()
{
    Sach s;
    s.xuLy("muon");
    s.xuLy("muon");        // muon cuon dang co nguoi muon?
    s.xuLy("tra");
    s.xuLy("het han");     // sach dang nam tren ke ma qua han?
    return 0;
}

Máy trạng thái viết bằng if chấp nhận chuyển trạng thái vô lý như mượn hai lần

enum class là kiểu liệt kê có phạm vi của C++11, các giá trị phải viết kèm tên kiểu như TrangThai::CoSan. Chương trình chạy, nhưng chấp nhận cả những chuyển trạng thái vô lý: mượn một cuốn đang có người mượn, và cuốn sách nằm trên kệ bỗng thành quá hạn. Code chỉ xét sự kiện, quên xét trạng thái hiện tại.

Muốn sửa đúng thì mỗi nhánh if phải lồng thêm một switch theo trạng thái. Với 3 trạng thái và 3 sự kiện là 9 trường hợp. Thêm trạng thái “đã đặt trước” là thêm một loạt trường hợp nữa, nằm rải trong nhiều hàm, rất dễ sót.

State pattern trong C++ là gì?

State pattern biến mỗi trạng thái thành một class. Class trạng thái tự quyết định phản ứng với từng sự kiện, kể cả chuyển sang trạng thái nào tiếp theo. Đối tượng chính (cuốn sách) chỉ giữ trạng thái hiện tại và chuyển sự kiện cho nó xử lý.

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

class Sach;

class TrangThai
{
public:
    virtual ~TrangThai() = default;
    virtual std::string ten() const = 0;
    virtual void muon(Sach&) const { std::cout << "  Khong muon duoc khi sach dang " << ten() << '\n'; }
    virtual void tra(Sach&) const { std::cout << "  Khong tra duoc khi sach dang " << ten() << '\n'; }
    virtual void hetHan(Sach&) const {}
};

class Sach
{
public:
    Sach();
    void doiTrangThai(std::unique_ptr<TrangThai> moi)
    {
        std::cout << "  " << trangThai_->ten() << " -> " << moi->ten() << '\n';
        trangThai_ = std::move(moi);
    }
    void muon()   { trangThai_->muon(*this); }
    void tra()    { trangThai_->tra(*this); }
    void hetHan() { trangThai_->hetHan(*this); }

private:
    std::unique_ptr<TrangThai> trangThai_;
};

class CoSan : public TrangThai
{
public:
    std::string ten() const override { return "co san"; }
    void muon(Sach& s) const override;
};

class DangMuon : public TrangThai
{
public:
    std::string ten() const override { return "dang muon"; }
    void tra(Sach& s) const override { s.doiTrangThai(std::make_unique<CoSan>()); }
    void hetHan(Sach& s) const override;
};

class QuaHan : public TrangThai
{
public:
    std::string ten() const override { return "qua han"; }
    void tra(Sach& s) const override
    {
        std::cout << "  Thu tien phat\n";
        s.doiTrangThai(std::make_unique<CoSan>());
    }
};

void CoSan::muon(Sach& s) const { s.doiTrangThai(std::make_unique<DangMuon>()); }
void DangMuon::hetHan(Sach& s) const { s.doiTrangThai(std::make_unique<QuaHan>()); }
Sach::Sach() : trangThai_(std::make_unique<CoSan>()) {}

int main()
{
    Sach s;
    std::cout << "muon:\n";     s.muon();
    std::cout << "muon:\n";     s.muon();
    std::cout << "het han:\n";  s.hetHan();
    std::cout << "tra:\n";      s.tra();
    std::cout << "het han:\n";  s.hetHan();
    std::cout << "tra:\n";      s.tra();
    return 0;
}

State pattern trong C++ từ chối chuyển trạng thái sai và thu tiền phạt khi trả sách quá hạn

Cùng chuỗi sự kiện như ví dụ switch, nhưng giờ các chuyển trạng thái vô lý đều bị từ chối: mượn lần hai báo “Khong muon duoc”, hết hạn khi sách đang có sẵn thì không có gì xảy ra, trả khi sách có sẵn thì báo lỗi. Trả sách quá hạn thì tự thu tiền phạt.

Cách hoạt động:

  • Lớp cha TrangThai có cài đặt mặc định là từ chối cho mọi sự kiện. Mỗi trạng thái con chỉ ghi đè những sự kiện hợp lệ với nó. Quên viết một sự kiện thì nó an toàn bị từ chối, chứ không chuyển bừa.
  • Trạng thái nhận Sach& để tự gọi doiTrangThai sang trạng thái tiếp theo. Toàn bộ quy tắc chuyển của trạng thái “đang mượn” nằm gọn trong class DangMuon.
  • Vì Sach và các trạng thái dùng lẫn nhau, mình khai báo trước class Sach; và viết thân vài hàm ở cuối file, sau khi mọi class đã được khai báo đầy đủ.

Lưu ý: doiTrangThai hủy đối tượng trạng thái cũ bằng unique_ptr, trong khi hàm của trạng thái cũ (CoSan::muon) vẫn đang chạy. Code an toàn vì ngay sau doiTrangThai không còn dùng thuộc tính nào của trạng thái cũ. Nếu trạng thái có thuộc tính riêng, phải đảm bảo không đụng tới chúng sau khi đã đổi trạng thái.

Bảng chuyển trạng thái

Với máy trạng thái nhỏ, một cách khác hay dùng trong lập trình nhúng là bảng chuyển trạng thái: một bảng 2 chiều, hàng là trạng thái, cột là sự kiện, mỗi ô là trạng thái tiếp theo.

Trạng thái \ Sự kiện muon tra hetHan
Có sẵn Đang mượn (từ chối) (bỏ qua)
Đang mượn (từ chối) Có sẵn Quá hạn
Quá hạn (từ chối) Có sẵn + phạt (bỏ qua)

Bảng này viết thẳng thành mảng 2 chiều trong code được, rất gọn, nhìn vào là thấy toàn bộ quy tắc. State pattern phù hợp hơn khi mỗi trạng thái có nhiều logic riêng (tính phạt, gửi thông báo, ghi log), bảng phù hợp khi chuyển trạng thái đơn giản và cần tốn ít bộ nhớ.

Dù chọn cách nào, hãy vẽ sơ đồ trạng thái trước khi viết code. Sơ đồ như hình đầu bài giúp phát hiện ngay các trường hợp còn thiếu.

Khi nào nên dùng State pattern?

  • Đối tượng có nhiều trạng thái, hành vi khác hẳn nhau theo trạng thái, và code đang đầy switch (trangThai) lặp lại ở nhiều hàm.
  • Quy tắc chuyển trạng thái phức tạp và hay thay đổi: tách mỗi trạng thái thành class giúp sửa một trạng thái không ảnh hưởng trạng thái khác.
  • Không cần khi chỉ có 2 trạng thái đơn giản (bật/tắt): một biến bool là đủ.

Bài tập

  1. Thêm trạng thái “Đã đặt trước”: sách đang mượn mà có người đặt trước, khi trả thì chuyển sang “Đã đặt trước” thay vì “Có sẵn”, và chỉ người đặt mới mượn được.
  2. Sửa ví dụ switch cho đúng: kiểm tra trạng thái hiện tại trước khi chuyển. Đếm xem phải viết bao nhiêu trường hợp.
  3. Viết máy trạng thái đèn giao thông (xanh 3 giây, vàng 1 giây, đỏ 2 giây) bằng bảng chuyển trạng thái, mô phỏng 10 giây và in trạng thái mỗi giây.

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

Kết

State pattern trong C++ biến mỗi trạng thái thành một class tự quyết định phản ứng với từng sự kiện, nhờ đó các chuyển trạng thái sai bị từ chối một cách tự nhiên và thêm trạng thái mới không phải sửa các trạng thái cũ. Với máy trạng thái đơn giản, bảng chuyển trạng thái là lựa chọn gọn hơn. Cả hai cách đều bắt đầu từ việc vẽ sơ đồ trạng thái cho đúng.

Tham khảo thêm ở Refactoring.Guru: State. Tới đây chúng ta đã hết chương design pattern. Chương cuối cùng sẽ ráp mọi thứ đã học thành các project hoàn chỉnh. 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 *