States (DevOps Sydney Mixer, 2013)

E34acb847338523dc088f03f0eedd1eb?s=47 Rob Howard
December 11, 2013

States (DevOps Sydney Mixer, 2013)

Thinking about an Order model as states and possible transitions; Finite State Machines made easy.

E34acb847338523dc088f03f0eedd1eb?s=128

Rob Howard

December 11, 2013
Tweet

Transcript

  1. States Avoiding if order.status == ‘placed’ || (order.status == ‘paid’

    && order.has_refunds_owing?); raise HelpLetMeOutOfHereError Hi! I’m Rob Howard, and I’m here to talk to you about States, or Finite State Machines. It’s not as boring as it sounds. :-)
  2. created_at 2013-11-22 09:00:01 paid_at 2013-11-22 09:00:01 cancelled_at NULL shipped_at NULL

    Okay, who’s written their own Shopping Cart app? Worked on one? Cool. Let’s think of an Order record. Has anyone seen anything like this?
  3. created_at 2013-11-22 09:00:01 paid_at 2013-11-22 09:00:01 cancelled_at NULL shipped_at NULL

    Okay, good. Now, let’s try to guess what the above represents.
  4. created_at 2013-11-22 09:00:01 paid_at 2013-11-22 09:00:01 cancelled_at NULL shipped_at NULL

    ... Paid? ... I’m going with Paid. I think. It’s been created and paid for.
  5. created_at 2013-11-22 09:00:01 paid_at 2013-11-22 09:00:01 cancelled_at 2013-11-24 08:10:01 shipped_at

    NULL Okay, what about this?
  6. ... Cancelled? created_at 2013-11-22 09:00:01 paid_at 2013-11-22 09:00:01 cancelled_at 2013-11-24

    08:10:01 shipped_at NULL ... Cancelled. Probably. Created, then paid for, then cancelled. (We’re making assumptions about the order the above all happened in.)
  7. created_at 2013-11-22 09:00:01 paid_at NULL cancelled_at NULL shipped_at 2013-11-22 09:00:01

    Now what about... Wait...
  8. created_at 2013-11-22 09:00:01 paid_at NULL cancelled_at NULL shipped_at 2013-11-22 09:00:01

    WHAT. Plainly something got all screwed up. Created and shipped with no payment date?
  9. created_at 2013-11-22 09:00:01 paid_at NULL cancelled_at NULL shipped_at NULL In

    any case, we can think of all these attributes...
  10. created_at 2013-11-22 09:00:01 paid_at NULL cancelled_at NULL shipped_at NULL ...

    as STATES, like so. An Order can be Created, Paid, Cancelled or Shipped. Let’s draw some pictures
  11. Placed This is “Placed”, our first state (or Starting State).

  12. Placed Paid Mark as Paid We can move to “Paid”,

    by “Marking as Paid”. This “Mark as Paid” is an event or a Transition.
  13. Placed Paid Shipped Shipped Mark as Paid Ship Similarly, we

    can “Ship” an order, transitioning to “Shipped”. In this system, this is an Accepting (or Final) State. The Order cannot and will not transition to any other state from this one. (We indicate this with the double-circle.)
  14. Placed Paid Shipped Shipped Mark as Paid Ship Note that

    you can’t jump from “Placed” to “Shipped”. We’re modeling the possible transitions.
  15. Placed Paid Shipped Shipped Mark as Paid Ship Just like

    a CMS page in a mandatory-review system can’t go from “Draft” to “Published” without an intermediary “Reviewed” state, in this system we can’t go to “Shipped” from “Placed” without “Paid” in the middle.
  16. Placed Paid Shipped Cancelled Shipped Shipped Mark as Paid Cancel

    Cancel Ship From “Placed”, we can also “Cancel”. Notice that we can transition to “Cancelled” from two states, “Placed” and “Paid”. This is important later.
  17. status “paid” You might be asking why we didn’t just

    have a “status” database column earlier. We can!
  18. status “paid” “shipped” “cancelled” That status column can contain “paid”,

    “shipped”, “cancelled”...
  19. status “paid” “shipped” “cancelled” We can keep a history by

    automatically creating an audit record whenever any transition happens.
  20. status “paid” “shipped” “cancelled” This greatly simplifies the model; instead

    of having the audit record being created in a model’s save() method (with conditional logic to check if it’s a state transition), we instead have the State Machine component of the model figure out when it needs to dump out a record.
  21. class Order include Workflow workflow do state :placed do event

    :cancel, transition_to: :cancelled event :mark_as_paid, transition_to: :paid end state :paid do event :cancel, transition_to: :cancelled event :ship, transition_to: :shipped end state :cancelled state :shipped end def mark_as_paid halt! 'No valid payment found' if payment_owed? end def cancel halt! 'Purchaser still owed' if refunds_owed? end #... Here’s an example of a state machine library in use. It’s Ruby, but you don’t need to really understand it to get what’s going on. Note the list of states inside workflow do ... end.
  22. class Order include Workflow workflow do state :placed do event

    :cancel, transition_to: :cancelled event :mark_as_paid, transition_to: :paid end state :paid do event :cancel, transition_to: :cancelled event :ship, transition_to: :shipped end state :cancelled state :shipped end def mark_as_paid halt! 'No valid payment found' if payment_owed? end def cancel halt! 'Purchaser still owed' if refunds_owed? end #... First, the “Placed” state, with two possible transitions: “cancel”, or “mark_as_paid”. It has a check (see def mark_as_paid) that prevents the transition from even happening if the conditions aren’t met.
  23. class Order include Workflow workflow do state :placed do event

    :cancel, transition_to: :cancelled event :mark_as_paid, transition_to: :paid end state :paid do event :cancel, transition_to: :cancelled event :ship, transition_to: :shipped end state :cancelled state :shipped end def mark_as_paid halt! 'No valid payment found' if payment_owed? end def cancel halt! 'Purchaser still owed' if refunds_owed? end #... Same with “Paid”. We can’t “Cancel” without having refunded the customer everything they’re owed first.
  24. class Order include Workflow workflow do state :placed do event

    :cancel, transition_to: :cancelled event :mark_as_paid, transition_to: :paid end state :paid do event :cancel, transition_to: :cancelled event :ship, transition_to: :shipped end state :cancelled state :shipped end def mark_as_paid halt! 'No valid payment found' if payment_owed? end def cancel halt! 'Purchaser still owed' if refunds_owed? end #... “Cancelled” and “Shipped” are our Accept/Final States. Neither have any way to transition back to other states. Let’s go back to the diagram.
  25. Placed Paid Shipped Cancelled Shipped Shipped Mark as Paid Cancel

    Cancel Ship By paying attention to the possible transitions, we can worry more about what we can do rather than the current state the Order is in.
  26. Placed Paid Shipped Cancelled Shipped Shipped Mark as Paid Cancel

    Cancel Ship
  27. if order.status == ‘placed’ || (order.status == ‘paid’ && !order.has_refunds_owing?)

    order.cancel! end And so this...
  28. if order.can_cancel? order.cancel! end ... Becomes this.

  29. None
  30. PHP Finite, FSM Ruby workflow, state_machine There are libraries for

    pretty much every language. It’s a concept, not a specific library, but these certainly make build FSMful models a lot easier.
  31. Fin. @damncabbage http://robhoward.id.au Thanks. :-)