Một state machine (máy trạng thái) có thể giữ tất cả các trạng thái có thể có của một cái gì đó và các chuyển đổi (transitions) được phép giữa các trạng thái này. Ví dụ, state machine cho một cánh cửa chỉ có hai trạng thái (open và closed) và chỉ hai chuyển đổi (opening và closing).
Ngược lại, state machine phức tạp có thể có vài trạng thái khác nhau với hàng trăm chuyển đổi giữa chúng. Thực tế, nếu bạn quan sát xung quanh, bạn sẽ nhận ra máy trạng thái hữu hạn (finite state machines) bao quanh bạn — khi mua hàng trên web, lấy gói snack từ máy bán hàng tự động, hoặc thậm chí chỉ rút tiền từ ATM.
Trong bài viết này, chúng ta sẽ xem cách thiết lập state machine trong Ruby và dùng gem state_machines.
State Machine trong phát triển
Khi nào chúng ta cần state machine trong phát triển? Câu trả lời đơn giản: bất cứ khi nào bạn muốn mô hình hóa nhiều quy tắc cho chuyển đổi trạng thái hoặc thực hiện side-effect (hiệu ứng phụ) trên một số chuyển đổi. Chìa khóa ở đây là xác định phần của ứng dụng sẽได้ lợi từ state machine. Ví dụ tốt luôn đúng với tôi là Order (đơn hàng) trong bối cảnh ứng dụng thương mại điện tử.
Đối với ứng dụng đơn giản bán sản phẩm trực tuyến, một Order có thể ở một trong các trạng thái sau:
created(đã tạo)processing(đang xử lý)ready(sẵn sàng)shipped(đã gửi)delivered(đã giao)void(hủy/bỏ)
Bạn có thể thấy các chuyển đổi được phép trong sơ đồ sau.

Hình dung đơn hàng dưới dạng state machine ngay lập tức cho phép hiểu rõ toàn bộ luồng, từ đặt hàng đến giao hàng. Và đối với chúng ta là nhà phát triển, nó cho phép các bước rõ ràng, đã định về những gì có thể và không thể làm tại các điểm cụ thể trong luồng đó.
Càng nói, dễ dàng rơi vào "hang hà" (rabbit hole) bằng cách chọn vấn đề quá rộng và kết thúc với hàng trăm trạng thái khó lý giải và theo dõi. Vì vậy, luôn có ý tưởng cấp cao và biểu đồ trạng thái (state chart) cho state machine đề xuất trước khi triển khai.
State Machine đầu tiên của chúng ta trong Ruby
Hãy thử triển khai OrderStateMachine với Ruby.
Đó là triển khai đủ đơn giản. Với mỗi chuyển đổi có thể có trong state machine, chúng ta định nghĩa một phương thức mới làm một số kiểm tra an toàn (sanity checks) và thực hiện chuyển đổi.
Điều tuyệt vời của triển khai trên là mọi thứ tường minh (explicit): bất kỳ nhà phát triển mới nào cũng có thể rất nhanh hiểu toàn bộ phạm vi của state machine.
Hãy xem cách thêm side-effect vào các chuyển đổi. Chúng ta sẽ tự động gửi (ship) đơn hàng đã đóng gói và có thể giao:
Mặc dù triển khai naїve (ngây thơ) hoạt động tốt, nhưng nó quá chồng chất (verbose), đặc biệt khi có nhiều chuyển đổi và điều kiện.
Kiểm tra nhanh trên Ruby Toolbox đưa ra một số gem cho state machines. state_machines và aasm là hai gem phổ biến nhất, và cả hai đều có adapter ActiveRecord nếu muốn dùng với Rails. Cả hai đều được kiểm tra kỹ và sẵn sàng production, nên hãy xem xét nếu bạn cần triển khai state machine.
Dùng gem State Machines trong Ruby
Cho bài viết này, tôi sẽ mô tả cách chúng ta có thể mô hình hóa state machine cho Order dùng gem state_machines.
Lớp trên định nghĩa state machine đơn giản của chúng ta và tất cả chuyển đổi có thể có. Bên cạnh đó, nó cũng tự động expose nhiều phương thức tiện ích trên một order:
Thực thi Side-effect với gem State Machines trong Ruby
Như thường lệ với bất kỳ hệ thống thực tế nào, nhiều chuyển đổi trong state machine sẽ đi kèm side-effect. Gem state_machines làm cho việc định nghĩa và thực thi chúng dễ dàng. Hãy thêm hai side-effect:
- Trên tất cả chuyển đổi đến
ready, tạo consignment giao hàng nếu orderdeliverable. - Trên tất cả sự kiện
fail_delivery, gửi email cho user thông báo sự kiện.
Chúng ta có thể thực hiện chuyển đổi như bình thường trong order:
Tôi đề xuất xem trang GitHub của gem state_machines để tìm hiểu tất cả tùy chọn gem cung cấp.
Dùng gem State Machines với ActiveRecord trong Ruby on Rails
Như đã thảo luận trước đó, cả state_machines và aasm hỗ trợ ActiveRecord. Khi dùng gem state_machines với ActiveRecord, không quá nhiều thứ thay đổi trong triển khai. Bạn có thể tiếp tục dùng hầu hết những gì đã thảo luận ở các phần trước.
Dưới đây là một số tính năng bổ sung bạn nhận được:
- Side-effect xảy ra bên trong transaction (giao dịch). Nghĩa là nếu bạn làm một số thao tác cơ sở dữ liệu bên trong transition hook và transaction thất bại, thay đổi trạng thái sẽ không được commit. Xem
use_transactionsnếu muốn tắt hành vi này. - Giống như mọi callback ActiveRecord khác, nếu bạn muốn cập nhật thuộc tính của model hiện tại từ một chuyển đổi, bạn phải làm điều này bên trong callback
before_transition. Để cập nhật thuộc tính bên trongafter_transition, bạn cần lưu model lại. Xem ví dụ sau:
- Gem tự động cung cấp scope (phạm vi) để lọc model theo trạng thái. Ví dụ, bạn có thể làm
Order.with_state(:processing)để tìm tất cả order đang xử lý hoặcOrder.without_state(:processing)để tìm tất cả order không ở trạng thái xử lý. - Nếu thay đổi trạng thái được thử mà không có chuyển đổi khớp (ví dụ: từ
processingđếndelivered), record sẽ không lưu được, và lỗi sẽ được thêm vào validation errors. Để quốc tế hóa (internationalize) thông báo lỗi sinh ra, chỉ cần thêm một số key để cung cấp bản dịch cho trạng thái và sự kiện bên trong file cấu hình (ví dụ:en.yml):
Ghi chú thêm: Xem bài này để khắc phục sự cố hiệu năng ActiveRecord.
Tổng kết: Xây dựng State Machine trong Ruby
Trong bài viết này, chúng ta khám phá tại sao dùng state machine trong phát triển, trước khi xây dựng một state machine đơn giản. Cuối cùng, chúng ta xem cách dùng gem state_machines trong Ruby và Ruby on Rails.
Bây giờ bạn đã có hiểu biết cơ bản về state machine, tôi chắc chắn bạn sẽ nhận ra một số kịch bản xung quanh đã dùng state machine hoặc sẽ hưởng lợi lớn từ việc dùng nó.
Cho đến lần sau, chúc bạn may mắn xây dựng state machines!
P.S. Nếu bạn muốn đọc bài Ruby Magic ngay khi xuất bản, hãy đăng ký newsletter Ruby Magic của chúng tôi và đừng bỏ lỡ bất kỳ bài nào!
Pulkit Goyal
Tác giả khách Pulkit là kỹ sư full-stack senior và cố vấn. Trong thời gian rảnh, anh viết về kinh nghiệm trên blog.
Tất cả bài viết của Pulkit Goyal