[Design Patterns #8] State Pattern

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ê →

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

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


Test class RentalRobot

Khi chạy chương trình trên, nếu bạn là vị khách may mắn thì kết quả sẽ là

Còn nếu bạn đen thì

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 

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

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 getCountsetCount để lấy ra và đút vào số lượng căn hộ còn trống


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

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

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.


Implement class WaitingState, ReceivingFormState, RentApartmentState, FullyRentedState


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

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) —