The Strategy Pattern in Ruby

Jan 4, 2022 - 5 min read
The Strategy Pattern in Ruby
Share

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.

SERIOUS ABOUT LEARNING RAILS?

Every other week you'll get at least 1 actionable tip on how to become a better Ruby on Rails developer.
    We won't send you spam. Unsubscribe at any time.
    Built with ConvertKit
    Cezar Halmagean
    Software development consultant with over a decade of experience in helping growing companies scale large Ruby on Rails applications. Has written about the process of building Ruby on Rails applications in RubyWeekly, SemaphoreCI, and Foundr.