Coding Dynamic Behavior with the Strategy Pattern
October 10, 2018
One of the benefits of object-oriented design is the ability for objects to share some behaviors while simultaneously differing in others. Typically, this is achieved through inheritance—when many subclasses inherit properties from a parent class but can optionally override certain behaviors as needed. This is a very useful and common design pattern; however, there are situations when polymorphism through inheritance is inappropriate. Consider, for example, when you only need a single behavior to change but otherwise want an object to stay the same. Or, when you want an object to shift its behavior at runtime based on some external (but unpredictable) factor. In such cases, an inheritance scheme is likely to cause unnecessarily bloated code that is difficult to maintain (particularly as the number of subtypes increases). However, there is a better approach: the strategy pattern.
Polymorphism through Strategies
The strategy pattern, also known as the policy pattern, is a behavioral design pattern that lets an object execute some algorithm (strategy) based on external context provided at runtime. This pattern is particularly useful when you have an object that needs to be able to execute a single behavior in different ways at different times. By using the strategy pattern, you can define a set of algorithms that can be dynamically provided to a particular object if/when they are needed. This pattern has a number of benefits, including: encapsulation of particular algorithms in their own classes; isolation of knowledge about how algorithms are implemented; and, code that is more flexible, mobile, and maintainable. To the last point, you may note that these are the same attributes that result from code that follows the Open/Closed Principle (OCP) and indeed, the strategy pattern is an excellent way to write OCP-adherent code.
When implementing the strategy pattern, you need three main elements:
- A client, which is aware of the existence of some abstract strategy but may not know what that strategy does or how it does it.
- A set of strategies that the client can use if/when provided with one of them. These may come in the form of first-class functions, objects, classes, or some other data structure.
- Optional context that the client can provide to its current strategy to use in execution.
The classic way to implement strategies is with interfaces. In this case, a client has an internal pointer to some abstract strategy interface, which is then pointed to a concrete strategy implementation via dependency injection (that is, during construction or with a setter at runtime). Thereafter, the client may use the provided strategy to carry out some work, all without knowing (or caring) what the strategy actually does.
Although interfaces are the classic way to implement the strategy pattern, a similar effect may be achieved in languages that do not have interfaces. What is important is that the client is aware of some abstract strategy and is able to execute that strategy without knowledge of its inner workings.
Polymorphism through Pure Inheritance
Before we look at how to use the strategy pattern, let’s look at a few examples that use other approaches to polymorphism. Consider the following snippet, which uses pure inheritance to define different types of runners in a race.
class Runner
attr_reader :name
def initialize(name)
@name = name
end
def run
puts "#{@name} is now running."
end
end
class Jogger < Runner
def run
puts "#{@name} is now jogging at a comfortable pace."
end
end
class Sprinter < Runner
def run
puts "#{@name} is now sprinting full speeed!"
end
end
class Marathoner < Runner
def run
puts "#{@name} is now running at a steady pace."
end
end
class Race
attr_reader :runners
def initialize(runners)
@runners = runners
end
def start
@runners.each { |runner| runner.run }
end
end
alice_ruby = Jogger.new("Alice Ruby")
florence_joyner = Sprinter.new("Florence Joyner")
eliud_kipchoge = Marathoner.new("Eliud Kipchoge")
race = Race.new([alice_ruby, florence_joyner, eliud_kipchoge])
race.start
# Alice Ruby is now jogging at a comfortable pace.
# Florence Joyner is now sprinting full speeed!
# Eliud Kipchoge is now running at a steady pace.
Here we have a Runner
parent class and three subclasses that inherit from it: Jogger
; Sprinter
; and, Marathoner
. Each subclass overrides the parent class’ run
method with its own implementation. Subsequently, when we instantiate runners of each type and pass them to a new Race object, we can see that each uses its own run
behavior when the race starts.
The above snippet works, but it has a few issues. First, it’s a bit bloated in that we have created many subclasses for the sole purpose of changing a single behavior. If the runners varied in more ways this might be worthwhile; however, in this simple program these classes are probably unnecessary. Another, perhaps more noteworthy, problem is that our runners are set permanently to a particular subclass. If, for example, alice_ruby
wanted to run like a Marathoner
, there is no good way to help her do that without completely changing her class.
If the ability to change behavior dynamically is desirable, then let’s look at one possible solution.
Naive Strategies and Control Flow
In an attempt to improve upon our earlier implementation of the runner program, below we have a refactored version that does not make use of inheritance.
class Runner
attr_reader :name, :strategy
attr_writer :strategy
def initialize(name, strategy)
@name = name
@strategy = strategy
end
def run
case @strategy
when :jog
puts "#{@name} is now jogging at a comfortable pace."
when :sprint
puts "#{@name} is now sprinting full speeed!"
when :marathon
puts "#{@name} is now running at a steady pace."
else
puts "#{@name} is now running."
end
end
end
class Race
attr_reader :runners
def initialize(runners)
@runners = runners
end
def start
@runners.each { |runner| runner.run }
end
end
alice_ruby = Runner.new("Alice Ruby", :jog)
florence_joyner = Runner.new("Florence Joyner", :sprint)
eliud_kipchoge = Runner.new("Eliud Kipchoge", :marathon)
race = Race.new([alice_ruby, florence_joyner, eliud_kipchoge])
race.start
# Alice Ruby is now jogging at a comfortable pace.
# Florence Joyner is now sprinting full speeed!
# Eliud Kipchoge is now running at a steady pace.
In this version, we have a single Runner
class with a constructor that accepts a new argument: a strategy
. In this case, our strategy
is just a symbol that we then use in a refactored run
method. The new run
method contains a case statement that checks on a given instance’s strategy
attribute and executes some bit of code accordingly. Indeed, when we start our race this time we get the same output as before.
In some ways, this version of the program is an improvement over our earlier version, though in others it is a step back. On the upside, we may now change a given runner’s naive strategy by using a setter to assign it a new symbol, as in alice_ruby.strategy = :marathon
. In this fashion, we’re able to effectively change the behavior of a particular object without changing its class. However, the long case statement in the Runner#run
method is problematic. Control flow of this sort is a clear violation of the OCP because we can’t extend the run method without opening it for modification. So what do we do if we want the ability to dynamically change strategies while still adhering to the OCP?
The Strategy Pattern in Action
In our final version of this program we’re finally going to use the strategy pattern. In this case, we define a set of strategies in their own classes and then provide those classes to our runners via dependency injection.
module RunStrategies
class RunStrategyInterface
def self.run(name)
raise "Run method has not been implemented!"
end
end
class Jog < RunStrategyInterface
def self.run(name)
puts "#{name} is now jogging at a comfortable pace."
end
end
class Sprint < RunStrategyInterface
def self.run(name)
puts "#{name} is now sprinting full speeed!"
end
end
class Marathon < RunStrategyInterface
def self.run(name)
puts "#{name} is now running at a steady pace."
end
end
end
class Runner
attr_reader :name, :strategy
attr_writer :strategy
def initialize(name, strategy)
@name = name
@strategy = strategy
end
def run
@strategy.run(@name)
end
end
class Race
attr_reader :runners
def initialize(runners)
@runners = runners
end
def start
@runners.each { |runner| runner.run }
end
end
alice_ruby = Runner.new("Alice Ruby", RunStrategies::Jog)
florence_joyner = Runner.new("Florence Joyner", RunStrategies::Sprint)
eliud_kipchoge = Runner.new("Eliud Kipchoge", RunStrategies::Marathon)
race = Race.new([alice_ruby, florence_joyner, eliud_kipchoge])
race.start
# Alice Ruby is now jogging at a comfortable pace.
# Florence Joyner is now sprinting full speeed!
# Eliud Kipchoge is now running at a steady pace.
As in our second snippet, our Runner
class accepts a strategy
argument at construction and also has a setter to change that strategy
if desired. However, instead of passing a simple symbol to Runner
to use in a control structure, we instead pass it one of several strategy classes defined in the RunStrategies
module. Each of these strategies has a run
method, meaning that our client objects can execute any of them with the same code. Since Ruby doesn’t have formal interfaces, we provide our own simple error checking mechanism by having each strategy inherit from a RunStrategyInterface
class that raises an error if its run
class method is called. (If a strategy fails to implement a version of this method on its own, then the RunStrategyInterface
run class method would execute and raise an error, which we could then test for prior to deployment.)
When this program runs, each runner is provided with the desired strategy at instantiation. During program execution, the runners are then able to use these strategies as needed, passing their own name as context to the strategy. And if we wanted to update a particular runner’s strategy mid-way through the program, we could easily do so with a setter method, as in alice_ruby.strategy = RunStrategies::Marathon
.
By using the strategy pattern, we have given our program the ability to dynamically change algorithms at runtime based on context. Further, our Runner#run
method is OCP-consistent because we can create new behaviors by simply implementing new strategies (rather than changing a control structure in the run method.)
TL;DR
The strategy pattern is a behavioral design pattern used to dynamically choose and execute algorithms at runtime. This pattern is particularly useful when a given class needs to execute the same behavior in different ways at different times. The strategy pattern allows a program to use polymorphism without a bloated class inheritance structure and still remain consistent with the Open/Closed Principle. Classically, the strategy pattern is implemented using interface abstractions, which let us create multiple strategy implementations to be passed to client objects as needed. However, it is possible to use the strategy pattern in languages without formal interfaces by following a set of conventions and implementing custom error checking.
References
- Blog: Strategy; Refactoring Guru
- Blog: Strategy; OODesign
- Blog: How to Use the Strategy Design Pattern in Ruby; RubyGuides
- Wikipedia: Strategy pattern
- Wikipedia: Design Patterns
That’s all for our discussion of the strategy pattern! Stay tuned for future blog posts on other design patterns such as the decorator pattern and the abstract factory pattern.
Note: This article was originally published on Medium.