What is a strategy pattern?
Strategy pattern is used to organize code and simplify similar code execution paths. This is achieved by creating a contract in the code that will ensure variations are independent and remain consistent. This makes the code easier to change.
Applications on your computer can be viewed as a simple analogy for thinking about the strategy pattern. Would you use a music player to edit a text file? How about a text editor to edit a photo? These examples sound ridiculous, but why?
Even without thinking about it, you know every app is going to behave in a certain way. Some apps are meant for photo editing, others are for listening to music. You know you can trust your apps to do what they’ve promised to do.
In object-oriented programming, this trust can be created with a contract, often called “interface” in languages like TypeScript, PHP, Java, and C#.
interface WebBrowser { open(url: string): void;}class Firefox implements WebBrowser { open(url: string): void { // ... }}class Chrome implements WebBrowser { open(url: string): void { // ... }}
Definition
Strategy pattern defines how to organize similar tasks and switch between them as necessary by implementing behaviors through contracts.
Purpose
Strategy pattern is often described as an interchangeable family of encapsulated algorithms. It’s a good start, but there’s more to it. It’s useful beyond just being interchangeable.
It encourages using composition over inheritance, making the overall codebase more flexible and open to change.
Implementation
1. Identify the behavior
Be on the lookout for situations where the application changes behavior based on a condition.
This is often expressed through the desire for a switch condition either through the switch
statement itself or a chain of if / else
conditions.
Another common signal is the desire to create an abstract class to share code between a couple of subclasses.
2. Define the behavior through a contract
Interfaces define what methods a class should have - they’re contracts that define the behavior.
The code that implements those interfaces is promising to fulfill these contracts. And just like contracts in real life - this creates trust that the task is going to be performed according to the contract.
3. Implement the behavior
Make sure that classes explicitly express their reliance on the contract.
This can be achieved with types and the implements
keyword.
Other uses
Even though the strategy pattern originates from Object-Oriented Programming, the ideas can be repurposed for functional or even procedural programming styles because at its core - strategy is simply a way to organize code in a consistent manner.
Learning about the pattern is also a wonderful opportunity to learn more about inheritance and composition, and that’s where we’ll start.
Example: Inheritance versus Composition
Because strategy pattern enables composition, it can often eliminate the need for inheritance. This section is going to illustrate the thought process and implementation starting with inheritance and gradually moving toward a strategy pattern.
Imagine a team working on a chat application and someone comes up with a great idea - a chatbot that can tell random jokes.
The team decides to create a bot that tells dad jokes as an experiment. And it only took a couple of lines of code:
class Dad_Joke_Bot { readonly name = 'Dad Joke Bot'; readonly avatar = 'dad-bot'; readonly description = 'Dad jokes are very punny.'; async getContent() { const request = await fetch('https://icanhazdadjoke.com/', { headers: { Accept: 'application/json' } }) const data = await request.json(); return data.joke; } async getMessage() { return { author: this.name, avatar: this.avatar, content: await this.getContent(), time: new Date().toLocaleTimeString(), } }}
Adding more features
A new chatbot idea has emerged, jokes must be balanced with some serious wisdom.
The decision is made to introduce a Stoicism_Bot
that provides a random quote from the stoic philosophy.
Both need to fetch content from APIs so it seems like a good idea to just use a bit of inheritance to share code:
abstract class Chatbot { abstract readonly name: string; abstract readonly avatar: string; abstract getContent(): Promise<string>; async getMessage(): Promise<Message> { return { author: this.name, avatar: this.avatar, content: await this.getContent(), time: new Date().toLocaleTimeString(), } }}
Stoicism_Bot
and Dad_Joke_Bot
both extend Chatbot
class that’s going to handle returning a consistently formatted message by relying on an abstract getContent
method.
The devs are testing it out and it seems to work great!
Going Viral
So far the project is an overwhelming success and started trending on rebbit and even FuzzBeed picked up on this. Chatbots are going viral!
Customers are happy, the team is happy, and even the board is happy! What could go wrong?
Dear Robot Chatroom developers,
We’ve noticed a huge spike in API Requests from your application.
Please limit your API requests to less than 250 per hour or we’ll be forced to ban requests from your application. You have until tomorrow morning to start rate-limiting your application.
If you’d like to purchase our Enterprise Premium Plus+ API plan, please do not hesitate to get in touch with our generous business representatives at [redacted] on Monday.
Best regards,
Your friendly API Service.
Maybe the Friday deployment wasn’t such a good idea after all. This is time sensitive and the team is now working overtime to implement changes.
The fastest solution is to just cut down on the number of requests immediately and add a to-do to figure out a real solution later.
Inheritance has made this simple.
Modifying the getMessage
method in Chatbot
allows adding caching without modifying each individual Chatbot.
Luckily the systems team has already provided a caching system package, so the only change needed is to implement caching for the API responses:
abstract class Chatbot { abstract readonly name: string; abstract readonly avatar: string; abstract getContent(): Promise<string>; async getMessage(): Promise<Message> { let content = Cache.get(this.name); if (!content) { content = await this.getContent(); Cache.set(this.name, content); } return { author: this.name, avatar: this.avatar, time: new Date().toLocaleTimeString(), content: content, }; }}
Piece of cake! Caching is implemented, the weekend crisis is averted!
Systems team not happy
The hotfix was rushed over the weekend and in the process, the team forgot to read the rules for using the caching library. They were supposed to track cache hits and misses.
The header is set by the request handler not visible in this example, but the documentation says a cache value should be passed via a property cacheHit
.
abstract class Chatbot { abstract readonly name: string; abstract readonly avatar: string; abstract getContent(): Promise<string>; async getMessage(): Promise<Message> { let content = Cache.get(this.name); let cachedMessage = `[${this.name}] Cache: HIT` if (!content) { content = await this.getContent(); Cache.set(this.name, content); cachedMessage = `[${this.name}] Cache: MISS`; } return { author: this.name, avatar: this.avatar, log: cachedMessage, time: new Date().toLocaleTimeString(), content: content, }; }}
And with that, there’s now a chatbot that caches API responses and logs cache hits and misses.
But the feature requests keep coming in…
Feature Request from the Business team
Someone on the business team came up with a brilliant idea - hire comedians to write custom jokes. That’s going to eliminate reliance on the joke API and provide a unique selling point for the bots taking the product to the next level!
All that’s needed is to create a Comedian_Bot
class and implement the getContent
method:
class Dad_Joke_Bot extends Chatbot { readonly name = 'Dad Joke Bot'; readonly avatar = 'dad-bot'; readonly description = 'Dad jokes are very punny.'; async getContent() { const request = await fetch('https://icanhazdadjoke.com/', { headers: { Accept: 'application/json' } }) const data = await request.json(); return data.joke; } }
The inheritance approach has been a success. Implementing each additional feature is just adding a bit of code here and there without any obvious downsides.
Until…
Business Team entered the chat
Comedian bot is a premium feature with paying customers, so it must fetch fresh jokes from the database without any caching.
Inheritance attempts
This introduces a problem. So far the code has been designed to reuse as much as possible through inheritance. Is there a way to reuse code without sacrificing flexibility?
Change is the one constant in software engineering, so it’s reasonable to assume that more feature requests are going to come in. How to prepare for an uncertain future?
The team discussed two ideas, but they both had a couple of downsides -
Idea #1: Modify getMessage
Only Dad_Joke
and Stoicism_Quote
bots should be cached.
Perhaps a list can be kept with bots that should be cached:
abstract class Chatbot { abstract readonly name: string; abstract readonly avatar: string; abstract getContent(): Promise<string>; async getMessage(): Promise<Message> { const cachedBots = [ 'Dad Joke Bot', 'Stoicism Bot', ]; let content; let cachedMessage = `[${this.name}] Cache: NONE`; if (cachedBots.includes(this.name)) { content = Cache.get(this.name); cachedMessage = `[${this.name}] Cache: HIT` } if (!content) { content = await this.getContent(); if (cachedBots.includes(this.name)) { Cache.set(this.name, content); } cachedMessage = `[${this.name}] Cache: MISS`; } return { author: this.name, avatar: this.avatar, log: cachedMessage, time: new Date().toLocaleTimeString(), content: content, }; }}
But this is problematic for several reasons:
Tight Coupling
Inheritance promises an easy way to share code between the related child objects.
However, Chatbot
is no longer only sharing code, it’s also responsible for the child classes, keeping track of their names, and which objects should be cached.
If new child classes are introduced, it’s likely that Chatbot
will have to change as well.
This is tight coupling - when a part of your system can’t change without forcing changes elsewhere.
Uncertain future
By creating a list of bots that should be cached, the assumption would be in the future most bots are going to be uncached. But what if most chatbots are going to be uncached?
Without knowing the future, it’s impossible to know with certainty if the list should keep track of cached bots or a list of uncached bots.
Breaking SRP
Initially, the expectation was that the child classes are going to deal with data fetching and Chatbot
is only going to provide an easy way to share common code.
getMessage
was only supposed to get information and not deal with setting, parsing, validating, etc.
But the Chatbot
class is now:
- Deciding where to get the message from (children or cache)
- Getting the message
- Deciding whether the message should be cached
- Returning the formatted message
For example, what if a feature request comes in to parse those messages and make sure that the APIs don’t include certain jokes? That might naturally fall into the getMessage
method as well, bloating the method even further.
Idea #2: Optional Methods
Another way to approach this would be to introduce overridable methods.
Imagine an isCachable
method that defaults to a disabled cache, and if the subclasses want to enable cache, they can do so by overriding the isCachable
method.
Then a new method like getLogMessage
can be introduced to organize the cache hit/miss code better.
And to make sure that the getMessage
method only does one thing, the cache logic can be moved into a new method called maybeGetCachedContent()
:
abstract class Chatbot { abstract readonly name: string; abstract readonly avatar: string; abstract readonly description: string; abstract getContent(): Promise<string>; isCachable(): boolean { return false; } getLogMessage(): string { if (!this.isCachable()) { return `[${this.name}] Cache: NONE`; } if (Cache.has(this.name)) { return `[${this.name}] Cache: HIT`; } return `[${this.name}] Cache: MISS`; } async maybeGetCachedJoke() { let content; if (this.isCachable()) { content = Cache.get(this.name) } if (!content) { content = await this.getContent(); if (this.isCachable()) { Cache.set(this.name, content); } } return content; } async getMessage(): Promise<Message> { return { author: this.name, avatar: this.avatar, log: this.getLogMessage(), time: new Date().toLocaleTimeString(), content: await this.maybeGetCachedJoke(), }; }}
But this too has a few downsides:
Hidden complexity
Moving cache logic out of getMessage
does help with SRP in that one method.
But what about maybeGetCachedContent
? It’s still juggling responsibility between getting the message from cache or a fresh message and caching it.
A more accurate name for the method would be getCachedJoke_or_getFreshJokeAndCacheIt
.
Splitting code into smaller chunks can make it look better on the surface and improve readability but doesn’t solve the actual design problem.
Inverted Tight Coupling
An overridable isCachable
method may initially seem better keeping a list of the classes that should be cached.
And it is more flexible because the parent class no longer needs to change when the child changes.
However, isCachable
is still forcing change on both parent and child classes, and is going to require all child classes to implement the isCachable
method.
All that has really changed is that the tightly coupled responsibility has been inverted slightly. The parent is now suggesting some rules, while the child has to either conform or ignore the method.
Even though the parent is no longer keeping a list of classes that should be cached each time a new Chatbot is added, the implementation is going to have to consider whether isCachable
should be overridden or not.
Bad ideas multiply
What if similar methods to isCachable
are introduced? For example, isWellReceived
, isMovieQuote
, botFunFact
, etc.
Inheritance isn’t able to scale in the face of change.
Strategy Pattern
The team has spent a lot of energy trying to make inheritance work. Maybe it’s time to step back and take a different strategy.
Chatbots share a set of properties, but they are going to fetch messages from different APIs. Even if slightly, their behaviors are different.
What all bots have in common is that they are going to produce content. This is something that can be established as a contract through an interface.
interface Get_Content { getContent(): Promise<string>}
Any class implementing Get_Joke
is going to promise to implement getContent
. This allows the Chatbot
class to require that this contract is implemented for any object passed to it.
class Chatbot { constructor( public name: string, public avatar: string, protected behavior: Get_Content, protected cache: Caching ) { } async getMessage(): Promise<Message> { return { author: this.name, avatar: this.avatar, content: await this.cache.getValue(this.behavior.getContent), log: this.cache.getStatusUpdate(), time: new Date().toLocaleTimeString(), } } }
This has removed unnecessary responsibilities from the Chatbot
class - it’s now free to assemble the information any way necessary, without worrying about the details.
It’s clear about the requirements and doesn’t try to predict the future.
This is why composition over inheritance is so powerful.
const stoicismBot = new Chatbot( 'Stoicism Bot', 'stoicism-bot', new Stoicism_Quote(), new Cached_Message('Stoicism Bot'));const dadJokeBot = new Chatbot( 'Dad Joke Bot', 'dad-bot', new Dad_Joke(), new Cached_Message('Dad Joke Bot'));const comedianBot = new Chatbot( 'Comedian Bot', 'comedian-bot', new Comedian_Joke(), new Uncached_Message('Comedian Bot'));
Set strategies dynamically
Strategy pattern doesn’t dictate that you must set your strategies via the constructor. They can also be set dynamically.
const dadBot = new Chatbot( 'Dad Joke Bot', 'dad-bot', new Dad_Joke() ),// Change the behavior to a dad jokedadBot.setContentBehavior(new Comedian_Joke());// You could also override the `getContent` method directlydadBot.getContent = new Comedian_Joke();
The Winning Strategy
Turns out that someone from the business team reached out to the API Service provider and the decision was reached to pay for the API service and increase the limits.
It wouldn’t be financially viable to do this for free, and a feature request has been made to introduce a premium tier.
Some of the Chatbot variations are going to have jokes that are objectively verified as funnier by a panel of experts that the free tier.
Premium Inheritance
With inheritance, Chatbot
class once again has no choice but to pick up more responsibility - in this case ensuring that the user is seeing the right message by checking their membership status.
Another abstract method has to be introduced to Chatbot
:
class Chatbot { abstract async getPremiumContent(): Promise<string> { // Bots that premium jokes should implement this method return; }}
Using that method could look something like this:
if (this.currentUser.premium) { content = await this.getPremiumContent();} else { content = await this.getContent();}
Read the code above carefully before you continue.
Think about this a little bit more.
Notice anything wrong?
You’re not alone, nobody noticed this in the code review either.
Premium users get no jokes at all from Chatbots that don’t have the premium joke method implemented! 😱
To squash the bug, the else
condition needs to be removed.
It’s not elegant, but it’ll do.
Adding a helpful comment for good measure too:
if (this.currentUser.premium) { content = await this.getPremiumContent();}// Do not use else here!// This should only run if the user is premium, but there's no premium content:if (!content) { content = await this.getContent();}
Well. At least it works, right?
Premium Strategy
Contrast the feature request with Strategy.
Creating Premium_Dad_Joke
that implements Get_Joke
interface is easy.
It can handle API Credentials as necessary and doesn’t need to worry about user permissions.
It’s neatly self-isolated.
Activating premium jokes can be done on the fly as necessary by replacing the Chatbot.behavior
:
const dadJokeBot = new Chatbot('Dad Joke Bot', 'dad-bot', new Dad_Joke());if (currentUser.premium) { dadJokeBot.behavior = new Premium_Dad_Joke();}
Conclusion
The strategy pattern is a great tool to have in your tool belt to keep your code flexible. It often removes the need for inheritance and conditionals.
However, this doesn’t always come for free. You may have noticed that the constructor has become pretty lengthy. Naturally, as the app development progresses the business team is going to come up with new ways of making money, and the devs will come up with better cache optimization.
Strategy pattern welcomes change, but at some point, it might become cumbersome to create new Chatbots. One might argue that having a lengthy constructor composing multiple classes is a signal that the ergonomics should already be improved.
But that’s a story for another pattern.