Ruby Gem AASM - Giải quyết bài toán chuyển trạng thái phức tạp dễ như trở bàn tay
Đã bao giờ bạn gặp tình huống phải xử lý việc chuyển trạng thái của các đối tượng, mà việc thay đổi trạng thái ấy có tính ràng buộc, có điều kiện, lại kèm theo một đống hook cần phải thực hiện với nó. Ví dụ một khóa học đang init (khởi tạo) bạn muốn cho nó sẵn sàng chạy thì chuyển trạng thái ...
Đã bao giờ bạn gặp tình huống phải xử lý việc chuyển trạng thái của các đối tượng, mà việc thay đổi trạng thái ấy có tính ràng buộc, có điều kiện, lại kèm theo một đống hook cần phải thực hiện với nó. Ví dụ một khóa học đang init (khởi tạo) bạn muốn cho nó sẵn sàng chạy thì chuyển trạng thái pending (chờ), sau khi khai giảng khóa học thì trạng thái cần chuyển sang in_progress (đang chạy), muốn kết thúc khóa học thì phải chuyển về trạng thái finished (kết thúc), muốn đóng khóa học trước khi kết thúc thì phải đưa trạng thái về closed (đóng). Ở đây về tính logic, rõ ràng bạn chỉ có thể cho chạy lớp học khi lớp đó đang chờ hoặc đang đóng. Bạn có thể kết thúc hay đóng, hoặc cho một lớp học vào trạng thái chờ nếu lớp đó đang chạy. Thêm nữa, mỗi khi chuyển trạng thái của khóa học, thông báo sẽ được gửi đến những người có vai trò nhất định trong khóa học đó (ví dụ như trainer và trainee cùng với giáo vụ). Khi kết thúc khóa học phải lưu lại thông tin về kết quả học tập của trainee.v.v Nếu bạn đang cố gắng thực hiện bài toán này bằng cách tạo ra một đống hàm, một chuỗi các biểu thức điều kiện và tạo ra callback ở khắp mọi nơi, thì mình tin rằng code của bạn sẽ trở thành một đống mess và chính bạn cũng tự nhận thấy rằng đó không phải là cách làm đúng.
Đây là lúc Máy trạng thái (State machine) xuất hiện và thể hiện những đặc điểm lợi thế của mình.
Máy trạng thái là một mô hình tính toán toán học. Nó là một máy trừu tượng luôn có trạng thái nằm trong tổng hữu hạng các trạng thái tại bất kỳ thời điểm nào. Máy trạng thái hữu hạn có thể chuyển từ trạng thái này sang trạng thái khác để phù hợp với đầu vào; sự thay đổi này được gọi là quá trình chuyển đổi. Máy trạng thái hữu hạn được xác định bởi danh sách các trạng thái của nó, trạng thái khởi đầu, và các điều kiện cho từng sự chuyển đổi trạng thái.
Bài toán ví dụ ở trên có thể được diễn tả lại bằng máy trạng thái như hình:
Trong đó:
- init, pending, in_progress, finished, closed là các trạng thái của khóa học
- ready, start, stop, finish, close là các sự kiện (event). Các sự kiện này phát sinh khi nhận các input như click lên button, hoặc được kích hoạt bằng job… Các sự kiện sẽ gây ra sự thay đổi trạng thái (ví dụ sự kiện start sẽ là từ pending -> in_progress), còn được gọi là quá trình chuyển đổi (transition)
Phần tiếp theo mình sẽ giới thiệu về việc áp dụng máy trạng thái để giải quyết bài toán chuyển đổi phức tạp này với gem aasm.
Gem này chứa gói AASM, một thư viện cho phép chúng ta thêm vào các class của Ruby một máy trạng thái hữu hạn (finite-state machine FSM). AASM là viết tắt của plugin acts_as_state_machine trước đây, hiện nay đã không còn sử dụng riêng cho ActiveRecord mà còn được tích hợp cho nhiều ORM khác. Một điều chắc chắn là nó có thể sử dụng cho bất cứ Ruby class nào dù cho parent class có là gì đi nữa.
Cài đặt gem
Bạn có thể cài đặt thủ công từ RubyGems.org
% gem install aasm
Hoặc là sử dụng Bundler
# Gemfile gem 'aasm'
Generators
Sau khi cài đặt xong bạn có thể sử dụng generator
% rails generate aasm NAME [COLUMN_NAME]
Nếu bạn đã có một trường để lưu trạng thái của model rồi thì không cần thực hiện bước này nữa. Nếu chưa thì bạn cần thực hiện câu lệnh trên và thay NAME là tên model. COLUMN_NAME là tên của trường trạng thái bạn cần dùng, có thể tùy chọn, vì mặc định nếu để trống thì nó sẽ được đăt là "aasm_state". Câu lệnh sẽ sinh ra model (nếu nó chưa tồn tại) và generate tự động đoạn block aasm (bạn sẽ thấy ở phần sau). Nếu bạn sử dụng Active Record thì nó sẽ đồng thời tạo một file migration để thêm trường trạng thái vào trong bảng.
Cách dùng
Giờ ta sẽ áp dụng máy trạng thái sử dụng gem aasm, bằng cách include module AASM, định nghĩa các state (trạng thái), events (sự kiện) cùng với các transitions (chuyển dịch) tương ứng.
class Course include AASM aasm do state :init, initial: true state :pending, :in_progress, :finished, :closed event :ready do transitions from: :init, to: :pending end event :start do transitions from: :pending, to: :in_progress end event :stop do transitions from: :in_progress, to: :pending end event :finish do transitions from: :in_progress, to: :finished end event :close do transitions from: :pending, to: :closed transitions from: :in_progress, to: :closed # có thể viết gộp lại như dưới đây # transitions from: [:pending, :in_progress], to: :closed end end end
Từ đây bạn có thể nhận thấy rằng chúng ta đang khai báo một máy trạng thái với khả năng cung cấp một cơ chế để quản lý ràng buộc các trạng thái, các sự kiện và chuyển đổi rất rõ ràng và tường minh. Khối lệnh trên sẽ cung cấp cho class Course một vài public methods như sau:
course = Course.new course.init? # => true course.may_ready? # => true course.ready course.pending? # => true course.init? # => false course.may_ready? # => false course.ready # => raises AASM::InvalidTransition
Khá là dễ để hiểu được ý nghĩa của các phương thức này, nên mình sẽ không giải thích quá nhiều. Để ý ở dòng cuối cùng, khi course đang ở trạng thái pending thì nó không thể thực hiện event ready, theo mặc định thì việc gọi đến một sự kiện mà không được phép sẽ raise ra lỗi AASM::InvalidTransition.
Tuy nhiên nếu bạn không thích exceptions mà muốn kết quả đơn giản là true hay false, thì chỉ cần thêm:
class Course ... aasm whiny_transitions: false do ... end end
whiny có nghĩa là càu nhàu, như vậy là bạn hiểu ý của option này là gì rồi đấy