Ruby & The Strategy Pattern4 min read

The Strategy pattern is the next in our examination of design patterns and how they can be leveraged in Ruby. This pattern is useful for varying parts of an algorithm at runtime, similarly to the Template Method pattern. Unlike the Template Method, which uses inheritance to change part of the algorithm, Strategy uses Dependency Injection.

It’s a powerful tool for keeping code maintainable by adhering to the “D” in SOLID, the Dependency Inversion Principle. DIP states that concretions (details) should depend on abstractions, rather than the other way around. By injecting a dependency at runtime we decouple our abstraction from the concrete implementations of its algorithm.

This can be really useful. By passing in our dependencies to a class at runtime, our class doesn’t have to care about what an object is. Instead, it only cares that it responds to a certain message that it wants to send.

When to Consider the Pattern

There are a number of scenarios where the Strategy pattern is worth considering:

  • The only difference between related classes is behavior.
  • Behavior can be defined at runtime.
  • You find yourself using conditional statements to do “type checking”.
  • You want to get rid of hard-coded dependencies.

Let’s look at an example.

Our First Solution

Let’s say we’re working on some piece of software that handles the logic for a game. This game is in very early development, so the requirements are very clear just yet. All we really know is that it’s a racing game so concepts like drivers and cars will likely make an appearance.

We’ve been handed a use case for a new feature that reads, “given a car, a driver should be able to accelerate up to a chosen speed.” That seems simple enough. Let’s use a sequence diagram to think things through.

Super simple! A Driver object seems to make sense, as does a Car since that’s the only type of vehicle we’ve been asked to account for. So we quickly write two classes to fulfill this use case.

class Driver
  attr_reader :car
  
  def initialize
    @car ||= Car.new
  end
  
  def floor_it
    car.accelerate_to(120)
  end
end   

class Car
  def initialize
    # some setup here
  end
  
  def accelerate_to(target_speed)
    while target_speed > current_speed
      # go faster
    end
  end
  
  # lots of other car stuff
end

This works fine; our tests pass, our use case is fulfilled. We commit the changes and move on to other things.

Strategy in Action

Then one day the product manager comes back with a new requirement. Early user panels indicate customers want a greater variety of vehicles than just cars. Right now, they just want to add motorcycles but the roadmap now envisions boats, planes, trains, and who knows what else.

Now we have a problem. We’ve hard-coded a dependency in our Driver class on a Car object. We need to 1) remove that dependency and 2) support any arbitrary kind of vehicle that can handle the behavior (acceleration, in our simple example).

The Strategy pattern to the rescue. We can fix both issues with one minor change: injecting the dependency instead of hard-coding it.

class Driver
  attr_reader :vehicle
  
  def initialize(vehicle)
    @vehicle = vehicle
  end
  
  def floor_it
    vehicle.accelerate_to(120)
  end
end

Now, whenever we call Driver.new we can pass in any kind of object as long as it responds to the accelerate_to message.

Our code can now handle any kind of vehicle and, what’s even better, is completely decoupled from any other object. Future changes are less likely to break this class (e.g., adding new vehicles). The Driver object is so resilient because it can be invoked in any context, and that context is actually what determines any dependencies. Consequently, testing this class is now much easier. You don’t have to set up the universe to be just so in order to get this object to behave.

Tradeoffs

The Strategy pattern helps you vary behavior of a larger algorithm without using a bunch of conditional, hard-coded dependencies, or type-checking. It’s not without cost, however. Dependencies still exist (objects have to talk to each other to do anything interesting), they’re just invoked by the context at runtime. Doing so may be cleaner and more maintainable in some cases, and messier in others. If it’s going to end up requiring shotgun surgery in the future because you have a change a bunch of different contexts, consider using a wrapping class or a different pattern altogether.

Conclusion

The Strategy pattern is a powerful tool for any developer to have access to. It’s great for decoupling code and keeping objects simple and context-independent. It’s one of the more commonly implemented design patterns, and one you’ll likely find useful in your career.

(Originally appeared on Medium).

I’m a full-stack web developer from Seattle. I love building software with an eye for quality and writing about the process.

Leave a Reply

Your email address will not be published. Required fields are marked *