Brief Overview of The Strategy Pattern
The Strategy or Policy Pattern is one of the behavioral design patterns from the gang of four design patterns book. It is primarily used when the specific implementation or algorithm cannot be determined until run time.
When To Implement
There are several common software design problems that are easily solved by implementing the strategy pattern:
- There are a number of classes that have the same attributes but vary in a specific method implementation. By implementing the strategy pattern a single method call can now have many behaviors.
- There is one action that can be implemented in various ways. The strategy pattern will allow for the correct implementation to be chosen at runtime.
- There is a class that has many different behaviors that are being determined by multiple if statements. The strategy pattern helps here by moving the related conditional branches into their own classes.
Strategy Pattern Components
The strategy pattern is typically comprised of three components and a client. The first component is the context which is used to return the correct concrete strategy at run time. The second component is a class that acts as the base strategy which is typically a parent class that has the strategy method implemented as an abstract method (the whole class is abstract). The third component is the concrete strategies which inherit from the parent strategy class and have specific implementations of the abstract method. The client will have a context and the context will have a reference to the strategy object and using polymorphism will be configured with the correct concrete strategy at run time.
Implementation of the Strategy Pattern
This implementation uses the basic structure of the design pattern.
The Context
The context is essentially used to determine which concrete strategy is needed by passing in some condition, for this example the temperature to convert from, and then instantiating it and allowing the client to interact with it using the sub class’s concrete implementation of the abstract method.
class TempConvertInterfaceContext #the initialize methods figures out the concrete strategy and then assigns it #to the convert strategy instance variable. Typically this would be an if #statement in some implementations but ruby allows us to do a little meta #programming. def initialize(convert_from) @convert_strategy = Object.const_get("#{convert_from.capitalize}Strategy").new end def convert_temperature(convert_to, temperature) @convert_strategy.convert_temperature(convert_to, temperature) end end
Abstract Interface For Concrete Strategies
All concrete instances of the strategy should inherit from this class and implement the abstract method so the context and client do not have to care about the implementation. The point of the interface is to define a method that must be implemented in all sub classes.
class StrategyInterface #In ruby we really don't need this interface at all. Ruby will throw #a NoMethod error if a method doesn't exist which is essentially what #I am doing here. For sake of sticking to the GOF implementation I will #use this class and method as an interface. def convert_temperature(convert_to, temperature) raise "Must implement this method." end end
Concrete Implementations of Strategy Interface
Each of these classes inherits from the StrategyInterface
class and implements the abstract convert_temperature
method.
class KelvinStrategy < StrategyInterface def convert_temperature(convert_to, temperature) case convert_to when 'celsius' temperature - 273.15 when 'fahrenheit' temperature * 9.0 / 5.0 - 459.67 when 'kelvin' temperature else raise "This Scale is currently not supported" end end end class FahrenheitStrategy < StrategyInterface def convert_temperature(convert_to, temperature) base_calculation = (temperature - 32) * 5.0 / 9.0 case convert_to when 'celsius' base_calculation when 'kelvin' base_calculation + 273.15 when 'fahrenheit' temperature else raise "This Scale is currently not supported" end end end class CelsiusStrategy < StrategyInterface def convert_temperature(convert_to, temperature) case convert_to when 'fahrenheit' temperature * 9.0 /5.0 + 32 when 'kelvin' temperature + 273.15 when 'celsius' temperature else raise "This Scale is currently not supported" end end end
The Client
The client is essentially the driving mechanism that needs to use the strategy but doesn’t know which of the strategies it will need until runtime.
class Client # The client has a context which is used to determine the # specific concrete strategy and then return that strategy back to the client. # The values in this array are different concrete strategies that # will be chosen at runtime to implement. ['fahrenheit_to_celsius', 'fahrenheit_to_kelvin', 'celsius_to_fahrenheit', 'celsius_to_kelvin', 'kelvin_to_celsius', 'kelvin_to_fahrenheit'].each do | temperature_scale_conversion_direction | #Parsing the string into the two temperature scales. convert_from, convert_to = temperature_scale_conversion_direction.split(/_to_/) #This instantiates the context with the #specific concrete strategy and returns the concrete strategy. temp_converter = TempConvertInterfaceContext.new(convert_from) #Printing the converted temperature. puts temp_converter.convert_temperature(convert_to, 0) end end
Get the Code
Here is the Github repository for the code. You will notice I also made another version more idiomatic to ruby using Procs.
Final Thoughts
The Strategy Pattern is extremely handy to use when you need to check the type of something and then perform an action on it. If the type is not known until runtime, using the context to return the correct strategy removes the burden from the client, who in all honesty should not care about the implementation at all.
Ruby Gem
I created a ruby gem from this post it can be found here. The source code for the gem can be located here.