Quantcast
Channel: Mojo Lingo
Viewing all articles
Browse latest Browse all 59

State Machines in Your Applications

$
0
0

As an application developer for Mojo Lingo, I've had the opportunity to work on many telephony applications in various stages: from an idealistic idea to complete mess of code. One of the most useful patterns I find myself using over and over again is the state machine.

What Are State Machines?

State machines, or more specifically finite-state machines in computer science terms, encapsulate the idea that an object:

  • Can be in one of a number of states at any given time
  • Can transition between states by a trigger
  • Has limitations on which transitions are allowable among states

A simple example of a state machine would be a traffic light. At any given time a traffic light can be in one state: red, yellow, or green. It can also only transition to certain states:

State Machine Diagram

  • red -> green
  • green -> yellow
  • yellow -> red

A standard traffic light cannot go from green to red, bypassing yellow, or yellow back down to green.

The behaviour of the object is enforced by the state and its transitions between states.

Why Should You Use Them?

State Machines allow us to describe the behaviour of an object in a simpler and finite way. Transitions, and their triggers or events, determine allowable states of any given instance.

There are several excellent gems for adding state machines to ruby projects, if you're looking for a good one to start with, try the state_machine gem.

Imagine we are modelling a phone call for our telephony application. A call is either answered or not, so we have a boolean answered in our model.

class PhoneCall
    attr_accessible :answered

    def initialize
        self.answered = false
    end
end

Now our application grows, we want to report if the call failed or just wasn't answered. So we add a boolean failed and maybe a failure_reason:

class PhoneCall
    attr_accessible :answered, :failed, :failure_reason

    def initialize
        self.answered = false
        self.failed = false
    end
end

This is kinda ugly: a call be both answered and failed at the same time.

Ok, so we implement methods to enforce these rules:

class PhoneCall
    attr_reader :answered, :failed, :failure_reason

    def initialize
        self.answered = false
        self.failed = false
    end

    def answer
        self.answered = true
    end

    def fail(reason)
        self.failed = true
        self.failure_reason = reason
    end
end

That's better, but a successfully answered call exposes the method failure_reason, which doesn't make sense in the context of an answered call. Without some meta-programming there's not much we can do, so we carry on.

Now we also want to track if the call is dialing or in progress, and the length of the call. So we start adding more methods, add more checks to each method for the various booleans, implement some custom validators to ensure a call cannot be failed and in_progress, etc.

Our model is starting to get complex, and as we add more states and attributes, it gets exponetially more complex.

Replacing with State Machines

It's obvious to us that a phone call can only be in one state at a time:

  • dialing
  • in_progress
  • completed
  • failed

And that only certain states can go to other states:

  • dialing -> in_progress or failed
  • in_progress -> completed

In addition, only certain things make sense at certain states:

  • failure_reason only makes sense if the call failed.
  • duration only makes sense for completed calls.

All of this behaviour can be easily described using a state machine instead of manually managing multiple booleans and flags:

# Using the state_machine gem.
class PhoneCall
    state_machine :state, :initial => :dialing do
        event :answered do
            transition :dialing => :in_progress
        end

        event :failure do
            transition :dialing => :failed
        end

        event :hangup do
            transition :in_progress => :completed
        end

        state :completed do
            def duration
                @end_time - @start_time
            end
        end

        state :failed do
            attr_accessible :failure_reason
        end

        before_transition :dialing => :in_progress, :do => :rec_start_time
        after_transition :in_progress => :completed, :do => :rec_end_time
    end

    def answered?
        in_progress? || completed?
    end

    private

    def rec_start_time
        @start_time = Time.now
    end

    def rec_end_time
        @end_time = Time.now
    end
end

While the class may have more lines, it is much more obvious what is happening. The state machine enforces a lot of the business logic we were trying to do manually with booleans, flags, and validators. By introducing the concept of states and events, the class's API is much more semantic. Extending functionality or adding more states is as simple as defining a new state or event. Testing your object's behaviour becomes simpler; be sure to unit test that your transition paths are enforced and any events are correctly invoked.

When Should You Use State Machines?

I have a few general things I consider for when to use state machines:

  • More than one boolean field in your class.
  • You have a status (or similar) attribute.
  • You allow the deletion of a instance (don't delete it, make a deleted state).
  • Your object has a defined process it follows.
  • If you're thinking about adding one.

In addition, state machines are usually quite easy to retrofit into a class that can benefit them. Don't be afraid to refactor one in!

Conclusion

State machines are a great tool at your disposal for simplifying the behaviour of classes. They might seem like a lot of work at first, but as your system and objects grow, they allow you to reduce complexity dramatically. You can concentrate on functionality of the object, rather than enforcing the rules of it's behaviour.

More Info

The post State Machines in Your Applications appeared first on Mojo Lingo.


Viewing all articles
Browse latest Browse all 59

Trending Articles