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.
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.)