Strategies and OCP
September 10, 2019
The Strategy Pattern and OCP
Recently I ran into a great use for a common OOP pattern: the Strategy Pattern. This led me to think about the Open/Closed Principle (OCP) and my interest in software design. The catalyst: I needed a background job to act on several different classes and for each class to implement a slightly different update operation.
First, let’s define the Strategy Pattern. At a high level, this pattern captures the concept that, at run-time, the code can execute a single algorithm out of multiple ones available. Pretty straightforward, right?
Extending system behavior without modifying the system
Second, what is OCP? Anyone who has been programming for any reasonable length of time has run across OOP’s manifesto of SOLID. OCP is the “O” in SOLID’s acronym and was first outlined in 1988(!) and further refined in the 90’s as the “(Polymorphic) Open/Closed Principle.” OCP states that “you should be able to extend the behavior of a system without having to modify that system.” This might be a bit obtuse at first glance and we’ll dive into that in a moment.
Third, we have an abstract concept that is foundational to the Strategy Pattern: the concept of an “interface.” Interfaces can be defined as, “a contract that an object adheres to.” To clarify, let’s say you have a couple of classes that do slightly different things, but they each have a single method which they’ll respond to (like a “run” method). We can say that these two classes abide by the same contract–they implement the “run” interface. Another, less code-oriented way to consider this concept uses an electrical outlet. As long as you implement, or adhere to, its interface (such as with metal prongs in the correct configuration) you’ll be able to draw power from the socket.
As an aside, it’s difficult to talk about interfaces and strategies without touching on Inversion of Control and Dependency Injection. These are easily topics unto their own but worth mentioning briefly. By implementing strategies, we’re extracting an object’s core, reusable logic into a standalone, new object. With this extraction complete, we have achieved Inversion of Control. Now, during runtime, we can insert the appropriate logic, a process called Dependency Injection. This run-time injection, coupled with Inversion of Control, allows the underlying object to be more flexible.
Here’s a high-level of where we are:
- Decided at run-time, strategies provide a way for the code to execute a single algorithm out of several available ones (expressed as full-fledged classes)
- OCP tells us to extend functionality of a system rather than prying it open and changing existing work
- Interfaces (or contracts) are an agreed upon message signature used by many (often similar) classes
Now let’s take things a step further and bring back the two example classes mentioned earlier. We indicated that these classes provide slightly different (or even vastly different) implementations sitting behind a “run” interface. It could be that when one of these classes receives “run” it will jump into action with a Rube Goldberg-esque task while the other runs a few basic operations. So our two strategy classes might look like this:
In order to close the loop on making use of the Strategy Pattern we’ll need a “caller” object–we’ll call it
GifsToSlack expects to receive a strategy class, often times in its constructor, and sends the provided strategy the “run” message.
GifsToSlack doesn’t care about the implementation details of any of the strategy classes which it might receive. All
GifsToSlack cares about is sending the “run” message to the strategy object provided and expects that the strategy will abides by the contract. Now let’s add
GifsToSlack to our totally non-contrived example:
And, to pull OCP back into the picture, we can see that we’ve abided by its tenet that we should seek to extend functionality rather than crack open classes and change them. With strategies we can simply pass in the logic we need executed, and by adhering to the correct interface, we can be confident that our new addition should function as expected.
When to Use The Strategy Pattern
Often with patterns, especially unfamiliar ones, the toughest part is recognizing when to implement them. I needed to:
- Define custom logic on a per-model basis
- Allow support for additional models in the future, without any current insight
- Make the solution easy to grok
It was clear that the Strategy Pattern served my needs. Making use of strategies allowed me to isolate specific logic for several models. Future developers are empowered to add additional strategy objects. And since the Strategy Pattern is well known, it should be recognizable and will help others understand the overall solution.