The Strategy design pattern allows you to provide different variations of an algorithm by injecting them as dependencies.
It's similar to the Template Method pattern which achieves the same thing using inheritance instead.
Here's a video version of this article if you prefer to watch instead:
And you can check out the code on Github if you want to try it out.
So the way the Strategy pattern works is, there is this wrapper object, called the context which holds a reference to one of the strategies that you want to use. And it's delegating work to that strategy without caring how it does it's thing.
So you have a context object, and a number of strategy objects that you can provide to the context object.
And you can add more strategies, or change the existing ones without touching the context.
That's adhering to the Open/Closed principle because you can extend your application without having to change code.
And to the Dependency Inversion principle which states that you should depend on abstractions (in this case the strategy interface, namely the vehicle), not concretions (in this case the strategies, namely the car, bike, and boat).
So let's look at an example.
require "strategy/car"
require "strategy/bike"
require "strategy/boat"
class Route
attr_writer :vehicle
def initialize(current_location, vehicle)
@current_location = current_location
@vehicle = vehicle # the default strategy
@destination = nil
end
def directions(destination)
@destination = destination
hours
end
private
def hours
@vehicle.calculate_route(@current_location, @destination)
end
end
# Client code
route = Route.new("Home", Strategy::Car.new)
puts "Via Car: #{route.directions('San Francisco')} hours"
route.vehicle = Strategy::Bike.new
puts "Via Bike: #{route.directions('San Francisco')} hours"
route.vehicle = Strategy::Boat.new
puts "Via Boat: #{route.directions('San Francisco')} hours"
I'm using a route class that simply calculates the time it would take to get to a destination from my current location.
Now this is not a real application, so the actual location is fake, but it's just an example. And I think it does a good job at that.
So we have this Route
class that delegates the work to a strategy we provide via the @vehicle
instance variable.
We can override the strategy by using the vehicle
setter method whenever we want to use a different strategy for calculating the time it takes to arrive to our destination.
I'm initializing the Route
object (which is the context object) with the Car
strategy.
And on the following lines, I'm switching from the car strategy to the Bike
strategy, and finally the Boat
strategy.
By running this code, you'll see the output is different for each of the strategies. It's basically a different algorithm to calculate the time it takes to get to the destination.
But from the perspective of the Route
class, the way each strategy works is irrelevant. As long as it returns the data it needs it's not important how it does it.
So that's nice because the strategies are decoupled from the context.
The context can work with any strategy that respects a contract. The contract, in this case, is it needs to have a calculate_route
method that requires two arguments, and it returns a printable value.
Now you could argue that this contract is rather loose, but that's beyond the topic of this article. It could definitely be improved.
Let's take a quick look at the strategy classes.
# strategy/vehicle.rb
module Strategy
class Vehicle
def calculate_route(source, destination)
raise("Not implemented")
end
end
end
The way we're enforcing the contract is via this Vehicle
interface, which is basically a parent class that requires its subclasses to implement the calculate_route
method. Otherwise it raises an exception.
Ok so each concrete strategy class inherits from this Vehicle
class and defines its own calculate_route
method.
# strategy/car.rb
require "strategy/vehicle"
module Strategy
class Car < Vehicle
def calculate_route(source, destination)
[source.length, destination.length].inject(&mul)
end
private
def mul = -> (a, b) { a * b }
end
end
And while these algorithms are not very fancy, the point I'm trying to make is that each strategy class can have a totally different implementation of the algorithm. The context class doesn't care.
So you'll see the Car
strategy multiplies the source name's length with the destination's.
The Bike
strategy does the same but it also squares the result and adds some error correction to it.
# strategy/bike.rb
require "strategy/vehicle"
module Strategy
class Bike < Vehicle
ERROR_CORRECTION = 5
def calculate_route(source, destination)
[source.length, destination.length].inject(&squared) + ERROR_CORRECTION
end
private
def squared = -> (a, b) { a * b ** 2 }
end
end
And finally the Boat
strategy cubes the result of multiplying the values.
# strategy/boat.rb
require "strategy/vehicle"
module Strategy
class Boat < Vehicle
def calculate_route(source, destination)
[source.length, destination.length].inject(&cubed)
end
private
def cubed = -> (a, b) { a * b ** 3 }
end
end
One thing to note about the client code is that it has to know about, and provide the strategies.
In other words, you're pushing some of the coupling to the client.