In this post we are going to explore the State design pattern.
A typical scenario where the State design pattern is particularly helpful is when an object of our application needs to behave differently on the basis of its State.
In our example, the Person class defines three methods. Each of this methods will run some logic that is specific to the person state.
Here is the code for this example:
class Person def initialize @age = 0 @state = :CHILD end def incr_age @age+=1; if (@age==18)
@state = :ADULT
end if (@age==65)
@state = :PENSIONER
end end def vote()
puts "Too young to vote" else puts "Vote accepted" end end def apply_for_buspass
puts "Pass granted" else puts "Too young for a bus pass" end end def conscript case @state
puts "Too old to be conscripted"
puts "Too young to be conscripted"
puts "Here's your gun" end end end
Every time a method of the Person class is called, a status check is made.
This code is very convoluted. Adding more states means adding more "if" statements in every single method. Adding new behavior means implement a new "case" statement.
The nature of this problem, suggest the use of the State pattern.
The state pattern, which closely resembles Strategy Pattern, is a behavioral software design pattern, also known as the objects for states pattern. This pattern is used to encapsulate varying behavior for the same routine based on an object's state object. This can be a cleaner way for an object to change its behavior at runtime without resorting to large monolithic conditional statements
The first step is to create a class for each State a Person can acquire.
We also create a superclass called State which could hold common behavior to all the states:
class State def vote
'this method should be implemented in a concrete subclass' end def apply_for_buspass
'this method should be implemented in a concrete subclass' end def conscript
'this method should be implemented in a concrete subclass' end def apply_for_medical_card
'this method should be implemented in a concrete subclass' end end
I will show you only the state class for the ChildState:
require_relative 'state.rb' class ChildState < State @@instance = ChildState.new private_class_method :new def self.instance return @@instance end def vote puts "Too young to vote" end def apply_for_buspass puts "Too young for a bus pass" end def conscript puts "Too young to be conscripted" end def apply_for_medical_card puts 'qualified for medical card' end end
The other one's will be very similar to this one but they will implement specific behavior based on the specific state.
If you read my post on the Singleton pattern, I am sure you recognized that I am also using the Singleton pattern in this example. The reason behind this decision is that we do not need to create a state object for each Person object we create.
States this application is modelling at moment, are three by design, so we need to create only three states instances globally. Using the Singleton Pattern here, will insure that when we create a new State, only a single instance will be created and eventually retrieved.
States in this application are stateless.
It can also be useful to have an utility class that returns a specific state for a Person when an age is passed in. I will call this class AgeStateContext:
require_relative 'adult_state.rb' require_relative 'child_state.rb' require_relative 'pensioner_state.rb' require_relative 'teenager_state.rb' class AgeStateContext def AgeStateContext.state(age) if (age>65) return PensionerState.instance end if (age>18) AdultState.instance end if (age>15) TeenagerState.instance end return ChildState.instance end end
Now the person class doesn't need to keep its state anymore.
It can just ask this utility class, which will answer with the state based on its age.
This is the new Person class
require_relative 'age_state_context.rb' require_relative 'state.rb' class Person def initialize @age = 0 end def incr_age @age+=1 end def vote()
.vote end def apply_for_buspass
.apply_for_buspass end def conscript
.conscript end def apply_for_medical_card
.apply_for_medical_card end end
It looks much simpler and readable.
we can surely import the AgeStateContext class statically and avoid to name it every time we need to call the state(@age) method.
We create a simple main for exercising this application:
p = Person.new for i in 1..80 p.incr_age(); p.apply_for_buspass(); p.vote(); p.conscript(); p.apply_for_medical_card(); end
This is the class diagram for this application:
HAPPY CODING !