Finding a Better State Pattern (in Ruby)

Wednesday, February 07, 2007 by Nate Murray.

Introduction

Gang of Four outlines a pattern for modifying an object depending on its state. This is called the State pattern. I'm not going to go into all the details here, so if you are unfamiliar with the State pattern I'd recommend looking here and here.

This post addresses how to implement the typical State pattern in Ruby and explores its consequences and alternatives.

Problem: An object's behavior needs to be modified depending on what state it is in.
Solution: Create a State object and delegate the functionality to that State object.

Traditional State Pattern

Gang of Four outlines a pattern for modifying an object depending on its state. This is typically done be creating an Abstract State and then subclassing it to create a Concrete State. The originating object, referred to as the Context then delegates the specific method to an instance of the Concrete State along with any information needed.

For example, say we have a Product and we want the inventory levels to vary depending on where we are selling the Product. While the actual inventory we have doesn't change, we may want to tell, say, eBay or Amazon, that the inventory is lower than what we actually have to prevent overselling.

Figure 1. is a traditional implementation of this.

A simple implementation would delegate the method to the state 100% of the time. However, in our system, assume that we only want to use the State if it exists and if it contains the method we are interested in. This allows us to have default behaviors in our Product object. This may not always be the case, but in our example it is.

In Ruby, this looks something like the following:

class Product
  attr_accessor :state

  def inventory
    if state && state.respond_to?(:inventory)
      return state.send(:inventory, self)
    else
      return @inventory
    end
  end

end # end Product

class AmazonProductState < ProductState
  
  # take the inventory from the Context object (the Product) and divide it by 2
  def inventory(context)
    context.inventory / 2
  end

end

Consequences

  • A consequence of this approach is that you have to write the lines if state && state.respond_to?(:inventory) return state.send(:inventory, self) every time. Ideally we wouldn't have to write this over and over for every method we want to delegate.

A slight improvement

What we could do is write a method that writes the delegation code for the method for us.


def define_state_method(name, &block)
  # ... 
end

# Then just call that method fo r
define_state_method :quantity_on_hand do
  return @inventory
end # writes #1 in effect

Consequences

  • Eliminates the repetition of #1
  • However, you still have to plan ahead to make this method be delegated to the State object.

Ideally, what we want to do is have any method overwritten for a particular instance when the state is set. We want to keep the same class, and we don't want to change the methods of other instances of that class.

Just extend it

We can achive the affect we are looking for by simply extending the class on our Product object. See below:

class AmazonProductState < ProductState
  def inventory
    @inventory / 2
  end
  
  def new_method
    "7 llamas"
  end
end

class Product
  ...
  def set_state(klass)
    self.instance_eval do
      extend klass
    end
  end
  ...
end
>> p = Product.find(1) # => #<Product:@id=1...>
   q = Product.find(1) # => #<Product:@id=1...>
   
   q.inventory # => 10

   p.set_state(AmazonProductState) # => nil
   p.inventory # => 5

   p.respond_to(:new_method) # => true
   q.respond_to(:new_method) # => false

Consequences

  • Allows us to override any method without having to plan ahead when designing the Product object
  • Allows us to add new methods that only exist in the state
  • The new state does not change the class of the object
  • The new state only affects particular instances of the object, not the whole class

Labels: , , , ,

Who we are:
The Pasadena Ruby Brigade is a group of programmers who are interested in Ruby. We enjoy sharing our ideas and code with anyone who is interested in this wonderful programming language.

Who can join:
Anyone! Just head over to our mailing list and drop us an email.

What we do:
We recently started a project over at RubyForge. This project is a group of programs written and maintained by our members that we thought could be beneficial to the whole community.

Projects

Downloads

Recent Posts

Archives