State Pattern cho phép một đối tượng có thể thay đổi hành vi (behavior) của nó dựa trên trạng thái bên trong (internal state). Do đó State Pattern phù hợp để áp dụng trong trường hợp hành vi một đối tượng phụ thuộc vào trạng thái của nó và nó phải thay đổi hành vi lúc runtime tùy thuộc vào từng trạng thái. Trong các hệ thống lớn và phức tạp thì áp dụng State Pattern sẽ giúp code của bạn sáng sủa, độc lập về logic, không phải check quá nhiều điều kiện if…else rồi switch…case loằng ngoằng, dễ maintain, dễ mở rộng hơn.
Hãy cùng nhau phân tích tình huống sau
Giả sử chúng ta đang cần build một con robot phục vụ việc cho thuê nhà. Nhiệm vụ của robot là chỉ ngồi ở sảnh đợi khách thuê nhà xuất hiện. Khi một khách thuê nhà đến, họ phải điền thông tin vào form trên màn hình robot và bấm submit form cho robot sau khi đã điền đầy đủ thông tin, robot sẽ kiểm tra thông tin. Nếu thông tin trong form được chấp thuận, robot sẽ nhè ra chìa khóa cho khách thuê; nếu không, robot sẽ nói với khách thuê nhà là họ bị từ chối và lại tiếp tục đợi người khách tiếp theo xuất hiện. Và mỗi khi robot cho thuê xong 1 căn hộ thì nó phải kiểm tra xem có còn căn hộ nào trống nữa không, nếu không còn thì nó sẽ không tiếp nhận thêm khách thuê nữa.
Tiếp tục phân tích bài toán bên trên thì ta thấy rằng con robot sẽ cần phải có 4 trạng thái như sau:- Đang chờ khách thuê nhà – Waiting
- Đang nhận form đăng ký – Receiving a form
- Tiến hành cho thuê một căn hộ – Rent an apartment
- Đã hết phòng – Fully rented
Việc chuyển đổi giữa các state (state transition) có thể biểu diễn bằng UML Statemachine Diagram như sau →
Cách tiếp cận truyền thống
Chưa vội phang ngay State Pattern vào, lần này chúng ta sẽ thử code theo cách truyền thống xem nó có vấn đề gì không đã, đâu phải cái gì cũng phang pattern vào mà tốt đúng không nào ?
Chúng ta sẽ tạo một class tên là RetalRobot và sử dụng biến member mState để lưu internal state của robot, biến mNumberOfApartments để lưu tổng số phòng có thể cho thuê →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class RentalRobot { private: enum eState { STATE_WAITING = 0, STATE_RECEIVING_FORM, STATE_RENT_APARTMENT, STATE_FULL_RENTED }; int mNumberOfApartments; int mState; public: RentalRobot(int numberOfApartments) { mNumberOfApartments = numberOfApartments; mState = STATE_WAITING; } . . . }; |
Tiếp theo là các hàm để check state hiện tại của robot và đưa ra xử lý tương ứng. Đầu tiên là hàm getForm, hàm này sẽ được call khi khác thuê đã điền đầy đủ thông tin vào form và bấm submit →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
class RentalRobot { private: enum eState { STATE_WAITING = 0, STATE_RECEIVING_FORM, STATE_RENT_APARTMENT, STATE_FULL_RENTED }; int mNumberOfApartments; int mState; public: RentalRobot(int numberOfApartments) { mNumberOfApartments = numberOfApartments; mState = STATE_WAITING; } void getForm() { switch (mState) { case STATE_WAITING: mState = STATE_RECEIVING_FORM; printf("Thanks for the form.\n"); break; case STATE_RECEIVING_FORM: printf("We already got your form.\n"); break; case STATE_RENT_APARTMENT: printf("Hang on, we’re renting you an apartment.\n"); break; case STATE_FULL_RENTED: printf("Sorry, we're fully rented.\n"); break; default: break; } } . . . }; |
Nếu robot nhận form đăng ký nhưng nó đang ở trong trạng thái STATE_FULL_RENTED (đã hết phòng) thì nó sẽ show ra “Sorry, we’re fully rented.” , nếu ở STATE_WAITING nó sẽ show “Thanks for the form.” đồng thời chuyển sang STATE_RECEIVING_FORM,… có lẽ nhìn vào code bạn sẽ hiểu ngay tắp lự.
Tương tự là hàm checkForm, nếu robot được yêu cầu check form trong khi nó ở STATE_WAITING thì nó sẽ show ra “You have to submit an form.” để yêu cầu khách nộp form đăng ký. Nếu robot đang ở STATE_RECEIVING_FORM thì nó cần phải check form và sau đó đưa ra quyết định chấp nhận (accept) hoặc từ chối (reject) khách thuê, việc này được giả lập bằng cách sử dụng một số random và check xem số random đó có lớn hơn 5 hay không, đồng thời cũng phải check xem còn phòng trống không nữa →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
class RentalRobot { private: enum eState { STATE_WAITING = 0, STATE_RECEIVING_FORM, STATE_RENT_APARTMENT, STATE_FULL_RENTED }; int mNumberOfApartments; int mState; std::random_device mRandomGenerator; public: RentalRobot(int numberOfApartments) { mNumberOfApartments = numberOfApartments; mState = STATE_WAITING; } void getForm() { switch (mState) { case STATE_WAITING: mState = STATE_RECEIVING_FORM; printf("Thanks for the form.\n"); break; case STATE_RECEIVING_FORM: printf("We already got your form.\n"); break; case STATE_RENT_APARTMENT: printf("Hang on, we’re renting you an apartment.\n"); break; case STATE_FULL_RENTED: printf("Sorry, we're fully rented.\n"); break; default: break; } } void checkForm() { switch (mState) { case STATE_WAITING: printf("You have to submit an form.\n"); break; case STATE_RECEIVING_FORM: { // simulate the form checking std::uniform_int_distribution<int> int_distribution(0, 9); int isFormOk = int_distribution(mRandomGenerator) > 5; // check result if (isFormOk) { printf("Congratulations, you were approved.\n"); mState = STATE_RENT_APARTMENT; rentApartment(); } else { printf("Sorry, you were not approved.\n"); mState = STATE_WAITING; } break; } case STATE_RENT_APARTMENT: printf("Hang on, we’re renting you an apartment.\n"); break; case STATE_FULL_RENTED: printf("Sorry, we're fully rented.\n"); break; default: break; } } void rentApartment() { switch (mState) { case STATE_WAITING: mState = STATE_RECEIVING_FORM; printf("Thanks for the form.\n"); break; case STATE_RECEIVING_FORM: printf("You must have your form checked.\n"); break; case STATE_RENT_APARTMENT: printf("Renting you an apartment....\n"); mNumberOfApartments--; dispenseKeys(); break; case STATE_FULL_RENTED: printf("Sorry, we're fully rented.\n"); break; default: break; } } void dispenseKeys() { switch (mState) { case STATE_WAITING: printf("You have to submit an form.\n"); break; case STATE_RECEIVING_FORM: printf("You must have your form checked.\n"); break; case STATE_RENT_APARTMENT: printf("Here are your keys!\n"); if (mNumberOfApartments > 0) { mState = STATE_FULL_RENTED; } else { mState = STATE_WAITING; } break; case STATE_FULL_RENTED: printf("Sorry, we're fully rented.\n"); break; default: break; } } }; |
Test class RentalRobot
1 2 3 4 5 6 7 |
int main() { RentalRobot robot(10); robot.getForm(); robot.checkForm(); return 0; } |
1 2 3 4 |
Thanks for the form. Congratulations, you were approved. Renting you an apartment.... Here are your keys! |
1 2 |
Thanks for the form. Sorry, you were not approved. |
Như vậy là code chạy ngon, nhưng bạn có thể nhận thấy ngay là code nó dài vkl ra, và hàm nào cũng phải check xem state hiện tại là state nào trong 4 cái state kia để đưa ra action tương ứng. Và với cách tiếp cận này, khi bạn thêm nhiều state nữa thì, mỗi phương thức sẽ ngày càng dài hơn, to hơn, rối hơn và dần dần thành một đống shit nát bét. Chính vì thế chúng ta cần đến State Pattern, hãy đi tiếp phần sau.
Áp dụng State Pattern
Chúng ta sẽ sử dụng object để đóng gói state lại thay vì lưu state bằng một biến và check như lúc trước, mỗi state sẽ được đóng thành một class riêng. Sau đó, việc chúng ta cần làm là sử dụng một con trỏ state trỏ đúng vào cái state object tương ứng với trạng thái hiện tại và call các method thông qua con trỏ đó.
Hãy nhìn vào hình bên trên, class RentalRobot sẽ dùng biến con trỏ mState để trỏ đến state object hiện tại, nó có thể trỏ đến đối tượng của 1 trong 4 object tương ứng với các class WaitingState, ReceivingFormState, RentApartmentState, FullyRentedState. Nhờ đó code của class RentalRobot sẽ trở nên đơn giản, nó chỉ cần call đến các method của state object thông qua mState, việc xử lý như nào thì do state object hiện tại đảm nhiệm.
Class Diagram
Tạo interface cho RentalRobot: IRentalRobot IRentalRobot cần phải có các method như getForm, checkForm như lúc đầu →
1 2 3 4 5 6 7 8 9 |
class IRentalRobot { public: virtual void getForm() = 0; virtual void checkForm() = 0; . . . }; |
Trong quá trình hoạt động của mình thì một state object có thể phải quyết định để chuyển state của robot sang một state khác. Ví dụ, state hiện tại của robot là WaitingState và bạn đưa cho nó 1 cái form thuê nhà thì object đó phải có trách nhiệm chuyển state hiện tại của robot sang ReceivingFormState. Vì vậy, để các state object có thể change được state của robot thì robot object cần phải có hàm setState, getState và các hàm để lấy ra 4 cái state object kia nữa →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class IRentalRobot { public: virtual void getForm() = 0; virtual void checkForm() = 0; virtual void setState(IState *state) = 0; virtual IState* getState() = 0; virtual IState* getWaitingState() = 0; virtual IState* getReceivingFormState() = 0; virtual IState* getRentApartmentState() = 0; virtual IState* getFullyRentedState() = 0; . . . }; |
IState là interface chung của các state: WaitingState, ReceivingFormState, RentApartmentState, FullyRentedState. Mình sẽ nói về nó ở phần sau.
Cuối cùng, các state object cũng cần phải biết còn bao nhiêu căn hộ còn trống (để kiểm tra xem robot có cần chuyển sang trạng thái sang FullyRentedState hay không sau khi một căn hộ được thuê), vì vậy IRentalRobot sẽ có thêm hàm getCount và setCount để lấy ra và đút vào số lượng căn hộ còn trống →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class IRentalRobot { public: virtual void getForm() = 0; virtual void checkForm() = 0; virtual void setState(IState *state) = 0; virtual IState* getState() = 0; virtual IState* getWaitingState() = 0; virtual IState* getReceivingFormState() = 0; virtual IState* getRentApartmentState() = 0; virtual IState* getFullyRentedState() = 0; virtual int getCount() = 0; virtual void setCount(int count) = 0; }; |
Implement class RentalRobot
Như vậy chúng ta đã define xong interface cho RentalRobot, giờ là lúc phải implement nó. RentalRobot phải kế thừa IRentalRobot và implement các methods mà IRentalRobot đã khai báo. Đầu tiên hãy xem constructor →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class RentalRobot : public IRentalRobot { private: IState* mCurentState; IState* mWaitingState; IState* mReceivingFormState; IState* mRentApartmentState; IState* mFullyRentedState; int mCount; public: RentalRobot(int count) { mCount = count; mWaitingState = new WaitingState(this); mReceivingFormState = new ReceivingFormState(this); mRentApartmentState = new RentApartmentState(this); mFullyRentedState = new FullyRentedState(this); mCurentState = mWaitingState; } . . . }; |
Nhìn vào constructor bạn sẽ thấy rằng ta cần vào pass vào cho nó tổng số lượng căn hộ có thể cho thuê. Trong constructor sẽ tạo ra instance tương ứng với 4 state: WaitingState, ReceivingFormState, RentApartmentState, FullyRentedState. State hiện tại của RentalRobot được lưu bởi biến con trỏ mWaitingState và được khởi tạo trỏ đến instance của WaitingState. Ngoài ra bạn cũng có thể nhận thấy là khi tạo instance của các state thì cũng cần phải truyền vào con trỏ this của đối tượng RentalRobot (đến phần code của state thì sẽ hiểu tại sao).
Tiếp theo chúng ta sẽ implement các method khác →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
class RentalRobot : public IRentalRobot { private: IState* mCurentState; IState* mWaitingState; IState* mReceivingFormState; IState* mRentApartmentState; IState* mFullyRentedState; int mCount; public: RentalRobot(int count) { mCount = count; mWaitingState = new WaitingState(this); mReceivingFormState = new ReceivingFormState(this); mRentApartmentState = new RentApartmentState(this); mFullyRentedState = new FullyRentedState(this); mCurentState = mWaitingState; } ~RentalRobot() { if (mWaitingState != nullptr) { delete mWaitingState; mWaitingState = nullptr; } if (mReceivingFormState != nullptr) { delete mReceivingFormState; mReceivingFormState = nullptr; } if (mRentApartmentState != nullptr) { delete mRentApartmentState; mRentApartmentState = nullptr; } if (mFullyRentedState != nullptr) { delete mFullyRentedState; mFullyRentedState = nullptr; } mCurentState = nullptr; } void getForm() { mCurentState->getForm(); } void checkForm() { mCurentState->checkForm(); } void rentApartment() { mCurentState->rentApartment(); } void setState(IState *state) { mCurentState = state; } IState* getState() { return mCurentState; } IState* getWaitingState() { return mWaitingState; } IState* getReceivingFormState() { return mReceivingFormState; } IState* getRentApartmentState() { return mRentApartmentState; } IState* getFullyRentedState() { return mFullyRentedState; } int getCount() { return mCount; } void setCount(int count) { mCount = count; } }; |
Các bạn để ý sẽ thấy code của hàm getForm, checkForm rất nhàn, cả làm cái dell gì ngoài việc call đến hàm tương ứng của current state. Bắt đầu thấy phê phê rồi 😘
Tạo interface cho các state: IState
Mỗi state object đều cần phải có các methods: getForm, checkForm, rentApplication, và dispenseKeys, nhưng tất nhiên là mỗi state sẽ xử lý theo các cách khác nhau.
1 2 3 4 5 6 7 8 |
class IState { public: virtual void getForm() = 0; virtual void checkForm() = 0; virtual void rentApartment() = 0; virtual void dispenseKeys() = 0; }; |
Implement class WaitingState, ReceivingFormState, RentApartmentState, FullyRentedState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
class WaitingState : public IState { private: IRentalRobot* mRobot; public: WaitingState(IRentalRobot *robot) { mRobot = robot; } void getForm() { mRobot->setState(mRobot->getReceivingFormState()); cout << "Thanks for the form." << endl; } void checkForm() { cout << "You have to submit an form." << endl; } void rentApartment() { cout << "You have to submit an form." << endl; } void dispenseKeys() { cout << "You have to submit an form." << endl; } }; class ReceivingFormState : public IState { private: IRentalRobot* mRobot; std::random_device mRandomGenerator; public: ReceivingFormState(IRentalRobot *robot) { mRobot = robot; } void getForm() { cout << "We already got your application." << endl; } void checkForm() { // simulate the form checking std::uniform_int_distribution<int> int_distribution(0, 9); bool isFormOk = (int_distribution(mRandomGenerator) > 5); if (isFormOk && mRobot->getCount() > 0) { cout << "Congratulations, you were approved." << endl; mRobot->setState(mRobot->getRentApartmentState()); mRobot->getState()->rentApartment(); } else { mRobot->setState(mRobot->getWaitingState()); cout << "Sorry, you were not approved." << endl; } } void rentApartment() { cout << "You must have your application checked." << endl; } void dispenseKeys() { cout << "You must have your application checked." << endl; } }; class RentApartmentState : public IState { private: IRentalRobot* mRobot; public: RentApartmentState(IRentalRobot *robot) { mRobot = robot; } void getForm() { cout << "Hang on, we’re renting you an apartment." << endl; } void checkForm() { cout << "Hang on, we’re renting you an apartment." << endl; } void rentApartment() { mRobot->setCount(mRobot->getCount() - 1); cout << "Renting you an apartment...." << endl; dispenseKeys(); } void dispenseKeys() { if (mRobot->getCount() > 0) { mRobot->setState(mRobot->getWaitingState()); } else { mRobot->setState(mRobot->getFullyRentedState()); } cout << "Here are your keys!" << endl; } }; class FullyRentedState : public IState { private: IRentalRobot* mRobot; public: FullyRentedState(IRentalRobot *robot) { mRobot = robot; } void getForm() { cout << "Sorry, we’re fully rented." << endl; } void checkForm() { cout << "Sorry, we’re fully rented." << endl; } void rentApartment() { cout << "Sorry, we’re fully rented." << endl; } void dispenseKeys() { cout << "Sorry, we’re fully rented." << endl; } }; |
Test
Như các bạn thấy, code có thể không ngắn hơn nhưng sáng sủa, rõ ràng, dễ đọc, dễ maintain hơn rất nhiều. Bây giờ chúng ta sẽ test hoạt động của code mới. Code test vẫn giữ nguyên như lúc đầu →
1 2 3 4 5 6 7 |
int main() { RentalRobot robot(10); robot.getForm(); robot.checkForm(); return 0; } |
Kết quả sẽ vẫn giống như cách tiếp cận nông dân lúc đầu thôi. Nhưng cái quan trọng ở đây là code của bạn bây giờ đã tách biệt và đóng gói được xử lý của các state ra các class khác nhau, code sạch sẽ, đẹp đẽ, dễ bảo trì và mở rộng hơn nhiều.
— Phạm Minh Tuấn (Shun) —