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: articles, design patterns, gof, mixins, ruby