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()
if @state==:CHILD
puts "Too young to vote" else puts "Vote accepted" end end def apply_for_buspass
if (@state==:PENSIONER)
puts "Pass granted" else puts "Too young for a bus pass" end end def conscript case @state
when :PENSIONER:
puts "Too old to be conscripted"
when :CHILD:
puts "Too young to be conscripted"
when :ADULT:
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
raise
'this method should be implemented in a concrete subclass' end def apply_for_buspass
raise
'this method should be implemented in a concrete subclass' end def conscript
raise
'this method should be implemented in a concrete subclass' end def apply_for_medical_card
raise
'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()
AgeStateContext.state(@age)
.vote end def apply_for_buspass
AgeStateContext.state(@age)
.apply_for_buspass end def conscript
AgeStateContext.state(@age)
.conscript end def apply_for_medical_card
AgeStateContext.state(@age)
.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 !
LUCA
No comments:
Post a Comment