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 = :ADULTend if (@age==65)@state = :PENSIONERend end def vote()if @state==:CHILDputs "Too young to vote" else puts "Vote accepted" end end def apply_for_buspassif (@state==:PENSIONER)puts "Pass granted" else puts "Too young for a bus pass" end end def conscript case @statewhen :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 voteraise'this method should be implemented in a concrete subclass' end def apply_for_buspassraise'this method should be implemented in a concrete subclass' end def conscriptraise'this method should be implemented in a concrete subclass' end def apply_for_medical_cardraise'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_buspassAgeStateContext.state(@age).apply_for_buspass end def conscriptAgeStateContext.state(@age).conscript end def apply_for_medical_cardAgeStateContext.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