DEV Community

Daniel Zambrano
Daniel Zambrano

Posted on

Implementing a State Machine as a service with aasm gem.

I recognise the gem aasm is one of the most useful gems when we want to explain how our objects or logic will change among states and which conditions will be. However, after many years I’ve seen how many developers are still shooting themselves with this gem.

The original code has been done in collaboration with Martin Madsen, the code present in this blog is a generalization from the original, still, it is a similar concept.

I’ve seen with this gem is how they use the ActiveRecord implementation for every case. Let’s be clear, this implementation is great when the state and dependencies related are only models and you are not creating circular dependencies.

Multiple times we are working with the next scenario:

  • We have a booking for a room when the user is starting this booking its state is draft.
  • Later on, the user is confirming the date we will change it by pre-reserved.
  • When the payment has been done the state will be booked. Finally, when the booking has been fulfilled, the status will be fulfilled.

Of course, we might have more transitions between those cases, but with just 4 this example will be enough.

ActiveRecord Implementation

Usually, your code if you are implementing the ActiveRecord approach your code it will look

class Booking < ApplicationRecord
  include AASM
  belongs_to :provider
  belongs_to :client
  aasm do
    state :draft, initial: true, after: :notify_draft
    state :pre_reserved
    state :booked, after: :notify_reservation
    state :fulfilled
    event :pre_reserve! do
      transitions from: :draft, 
        to: :pre_reserved, 
        after: :notify_draft
    end
    event :book do
      transitions from: :pre_reserved,
        to: :booked,
        after: :process_book
    end
  end

  def notify_draft
    Bookings::Email.send(provider, 'Someone is interested')
  end

  def process_book
    Bookings::Payments.release(self)
    Bookings::Email.send(client, 'your reservation has been done')
    Bookings::Email.send(provider, 'you got a new reservation')
  end
end

Pros and Cons

  • Booking model is prone to become a god object.
  • Single responsibility principle is being violated. After changing states we are adding business logic. Booking is responsible of persistency, changing states(we can consider this as persistency if we don’t want to be strict), sending emails -when sending emails-, and releasing payments.
  • Testing will become harder, more items to stub.
  • Wrong dependency direction. Entities now will know about other services. Therefore, more dependencies.

State machine as a service.

A great approach is implementing the state machine as a service and injecting the booking we want to modify.

module Bookings
  class StateService
    include AASM
    attr_reader :booking

    def initialize(booking)
      @booking = booking

      # we need to initialize the state machine base on the current booking
      aasm.current_state = booking.status.to_sym
    end

    aasm do
      state :draft, initial: true, after: :notify_draft
      state :pre_reserved
      state :booked, after: :notify_reservation
      state :fulfilled

     # This callback could be something different, will dependent of your code
     after_all_transitions :save_state
      event :pre_reserve! do
        transitions from: :draft, 
          to: :pre_reserved, 
          after: :notify_draft
      end

      event :book do
        transitions from: :pre_reserved,
          to: :booked,
          after: :process_book
      end
    end

    def notify_draft
      Bookings::Email.send(provider, 'Someone is interested')
    end

    def process_book
      Bookings::Payments.release(self)
      Bookings::Email.send(booking.client, 'your reservation has been done')
      Bookings::Email.send(booking.provider, "you've got a new reservation")
    end

    def save_state 
      booking.update_attributes!(status: aasm.to_state) 
    end
  end
end

To achieve this implementation we only need to be aware of 2 sections. Everything else is your own code

The first piece of code is your initialiser, you will need to indicate what is the current state for you machine, your instance should have the value. Don't let you misguide you as a default value, it is the value for your entity.

def initialize(instance)
 aasm.current_state = instance.status.to_sym
end

Also when your changes are done, you need to make sure you will make it persistent, it is not mandatory this hook, but saving is a must.

aasm do
  after_all_transitions :save_state
end

def save_state 
  instance.update_attributes!(status: aasm.to_state) 
end

Pros

Considering this approach we will get two huge benefits:

  • Booking model is responsible only data persistency
  • This StateService is our actual State machine and the dependency direction is proper, because is a service knowing about other services, and the model booking won’t know about Email or Payments

Top comments (2)

Collapse
 
zipdevil profile image
Jasper Lin

Do you have an example of how to call the events/transitions in a controller?
I'm having trouble calling bang methods after moving AASM block outside of a model class.

Collapse
 
caciquecoder profile image
Daniel Zambrano

Sorry I haven't been active here. I will provide you an example soon