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
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.
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.
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 Sr. Software Engineer with Voom/Airbus from Seattle. I love building software with an eye for quality and writing about the process.