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.

Definitions can vary. Click to here for other definitions.

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.

Channels
#
announcements
#
weheartmoney
#
devchat
#
wip
#
watercooler
#
testing

Online
sil's avatar
Sil
devbeard96's avatar
the_beard
bizman's avatar
BizMan
busystems's avatar
busystems
captainshiphard's avatar
Captain Shiphard
chillbeets's avatar
ChillBeets
dog's avatar
unklear
#
testing
New Channel Created
devbeard96's avatar
the_beard
testing
6:26:41 PM
devbeard96's avatar
the_beard
testing again
6:30:58 PM
devbeard96's avatar
the_beard
/bot
6:33:04 PM
devbeard96's avatar
the_beard
/bot test
6:35:24 PM
devbeard96's avatar
the_beard
/bot test again
6:39:32 PM
devbeard96's avatar
the_beard
/bot test again again
6:43:30 PM
devbeard96's avatar
the_beard
why is this not working?
6:45:37 PM
devbeard96's avatar
the_beard
help. @Sil? Any ideas?
6:49:35 PM
sil's avatar
Sil
did you run the post-deploy script to bust all caches?
6:51:23 PM
devbeard96's avatar
the_beard
oh, right, I always forget...
6:53:21 PM
devbeard96's avatar
the_beard
/bot
6:55:53 PM
devbeard96's avatar
the_beard
Oh there we go,
6:59:04 PM
devbeard96's avatar
the_beard
/bot
7:01:05 PM
Unread Messages
devbeard96's avatar
the_beard
Hey wouldn't it be cool to have a chatbot that can tell dad jokes?
8:12:44 PM
sil's avatar
Sil
Seriously? Dad jokes?
8:14:04 PM
devbeard96's avatar
the_beard
Sure, why not?
8:15:50 PM
sil's avatar
Sil
They're pure cringe
8:18:31 PM
devbeard96's avatar
the_beard
Idk, I think I'll try it out anyway.
8:21:41 PM
sil's avatar
Sil
Do what you want, @captain isn't online anyway.
8:24:54 PM
devbeard96's avatar
the_beard
Cool. Give me a moment.
8:29:21 PM
devbeard96's avatar
the_beard
Ok, let's see if this works...
8:34:17 PM
devbeard96's avatar
the_beard
/bots
8:36:49 PM
abstract's avatar
Dumb Test Bot
I make a funny joke
8:38:07 PM
devbeard96's avatar
the_beard
YASS!
8:39:12 PM
sil's avatar
Sil
What?
8:44:10 PM
devbeard96's avatar
the_beard
Try typing /bots - that'll bring up the joke pop-up!
8:49:02 PM
abstract's avatar
Dumb Test Bot
I make a funny joke
8:52:15 PM
sil's avatar
Sil
Looks cool, but it needs real jokes...
8:56:35 PM
Available Robots
dad-bot's avatar
Dad Joke Bot
Dad jokes are very punny.
+

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!

Channels
#
announcements
#
weheartmoney
#
devchat
#
wip
#
watercooler
#
testing

Online
chillbeets's avatar
ChillBeets
devbeard96's avatar
the_beard
bizman's avatar
BizMan
busystems's avatar
busystems
captainshiphard's avatar
Captain Shiphard
sil's avatar
Sil
dog's avatar
unklear
#
testing
New Channel Created
devbeard96's avatar
the_beard
testing
6:20:51 PM
devbeard96's avatar
the_beard
testing again
6:24:04 PM
devbeard96's avatar
the_beard
/bot
6:26:50 PM
devbeard96's avatar
the_beard
/bot test
6:30:28 PM
devbeard96's avatar
the_beard
/bot test again
6:33:03 PM
devbeard96's avatar
the_beard
/bot test again again
6:37:39 PM
devbeard96's avatar
the_beard
why is this not working?
6:39:18 PM
devbeard96's avatar
the_beard
help. @Sil? Any ideas?
6:42:51 PM
sil's avatar
Sil
did you run the post-deploy script to bust all caches?
6:46:32 PM
devbeard96's avatar
the_beard
oh, right, I always forget...
6:47:45 PM
devbeard96's avatar
the_beard
/bot
6:52:38 PM
devbeard96's avatar
the_beard
Oh there we go,
6:56:33 PM
devbeard96's avatar
the_beard
/bot
7:01:05 PM
Unread Messages
devbeard96's avatar
the_beard
/bots
8:47:20 PM
dad-bot's avatar
Dad Joke Bot
When do doctors get angry? When they run out of patients.
8:51:20 PM
devbeard96's avatar
the_beard
Wonderful!
8:55:03 PM
devbeard96's avatar
the_beard
@chill I'm going to going to deploy and head off for the weekend!
8:56:32 PM
chillbeets's avatar
ChillBeets
You sure you want to ship on a Friday?
8:57:54 PM
devbeard96's avatar
the_beard
pfft, it's a small change, it'll be ok.
9:01:05 PM
+

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?

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.

Channels
#
announcements
#
weheartmoney
#
devchat
#
wip
#
watercooler
#
testing

Online
busystems's avatar
busystems
devbeard96's avatar
the_beard
bizman's avatar
BizMan
captainshiphard's avatar
Captain Shiphard
chillbeets's avatar
ChillBeets
sil's avatar
Sil
dog's avatar
unklear
#
testing
New Channel Created
devbeard96's avatar
the_beard
testing
6:21:32 PM
devbeard96's avatar
the_beard
testing again
6:25:21 PM
devbeard96's avatar
the_beard
/bot
6:29:27 PM
devbeard96's avatar
the_beard
/bot test
6:34:11 PM
devbeard96's avatar
the_beard
/bot test again
6:37:29 PM
devbeard96's avatar
the_beard
/bot test again again
6:40:42 PM
devbeard96's avatar
the_beard
why is this not working?
6:41:54 PM
devbeard96's avatar
the_beard
help. @Sil? Any ideas?
6:46:22 PM
sil's avatar
Sil
did you run the post-deploy script to bust all caches?
6:49:07 PM
devbeard96's avatar
the_beard
oh, right, I always forget...
6:53:34 PM
devbeard96's avatar
the_beard
/bot
6:58:19 PM
devbeard96's avatar
the_beard
Oh there we go,
6:59:43 PM
devbeard96's avatar
the_beard
/bot
7:01:05 PM
Unread Messages
busystems's avatar
busystems
@channel You're using cache, but you're not logging cache hits and misses.
8:43:49 PM
devbeard96's avatar
the_beard
We should be logging those?
8:46:19 PM
busystems's avatar
busystems
yup.
8:47:53 PM
devbeard96's avatar
the_beard
But why? Isn't that the responsibility of the caching system?
8:49:21 PM
busystems's avatar
busystems
No.
8:53:50 PM
devbeard96's avatar
the_beard
Anyway, I'm glad I did this with OOP - I just changed one file and the necessary methods trickle down easily.
8:56:45 PM
Available Robots
stoicism-bot's avatar
Stoicism Bot
Listening to this bot is the beginning of all wisdom.
dad-bot's avatar
Dad Joke Bot
Dad jokes are very punny.
+

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.

Channels
#
announcements
#
weheartmoney
#
devchat
#
wip
#
watercooler
#
testing

Online
devbeard96's avatar
the_beard
bizman's avatar
BizMan
busystems's avatar
busystems
captainshiphard's avatar
Captain Shiphard
chillbeets's avatar
ChillBeets
sil's avatar
Sil
dog's avatar
unklear
#
testing
New Channel Created
devbeard96's avatar
the_beard
testing
6:21:11 PM
devbeard96's avatar
the_beard
testing again
6:23:14 PM
devbeard96's avatar
the_beard
/bot
6:27:28 PM
devbeard96's avatar
the_beard
/bot test
6:28:44 PM
devbeard96's avatar
the_beard
/bot test again
6:31:45 PM
devbeard96's avatar
the_beard
/bot test again again
6:34:59 PM
devbeard96's avatar
the_beard
why is this not working?
6:39:50 PM
devbeard96's avatar
the_beard
help. @Sil? Any ideas?
6:44:20 PM
sil's avatar
Sil
did you run the post-deploy script to bust all caches?
6:48:48 PM
devbeard96's avatar
the_beard
oh, right, I always forget...
6:49:58 PM
devbeard96's avatar
the_beard
/bot
6:54:45 PM
devbeard96's avatar
the_beard
Oh there we go,
6:59:34 PM
devbeard96's avatar
the_beard
/bot
7:01:05 PM
Unread Messages
Available Robots
stoicism-bot's avatar
Stoicism Bot
Listening to this bot is the beginning of all wisdom.
dad-bot's avatar
Dad Joke Bot
Dad jokes are very punny.
+
Logana Konsole
> [Stoicism Bot] Cache: MISS
> [Stoicism Bot] Cache: HIT

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.

Channels
#
announcements
#
weheartmoney
#
devchat
#
wip
#
watercooler
#
testing

Online
bizman's avatar
BizMan
devbeard96's avatar
the_beard
busystems's avatar
busystems
captainshiphard's avatar
Captain Shiphard
chillbeets's avatar
ChillBeets
sil's avatar
Sil
dog's avatar
unklear
#
testing
New Channel Created
devbeard96's avatar
the_beard
testing
6:22:40 PM
devbeard96's avatar
the_beard
testing again
6:25:18 PM
devbeard96's avatar
the_beard
/bot
6:27:31 PM
devbeard96's avatar
the_beard
/bot test
6:30:55 PM
devbeard96's avatar
the_beard
/bot test again
6:35:27 PM
devbeard96's avatar
the_beard
/bot test again again
6:37:52 PM
devbeard96's avatar
the_beard
why is this not working?
6:40:50 PM
devbeard96's avatar
the_beard
help. @Sil? Any ideas?
6:42:08 PM
sil's avatar
Sil
did you run the post-deploy script to bust all caches?
6:45:25 PM
devbeard96's avatar
the_beard
oh, right, I always forget...
6:50:05 PM
devbeard96's avatar
the_beard
/bot
6:54:41 PM
devbeard96's avatar
the_beard
Oh there we go,
6:57:04 PM
devbeard96's avatar
the_beard
/bot
7:01:05 PM
Unread Messages
devbeard96's avatar
the_beard
/bots
8:38:55 PM
abstract's avatar
Dumb Test Bot
@TODO
8:41:34 PM
bizman's avatar
BizMan
Hey! How are you doing today?
8:44:27 PM
devbeard96's avatar
the_beard
I'm doing great! How about you?
8:45:41 PM
bizman's avatar
BizMan
Oh we're doing great! We're really excited about the new comedian bot!
8:48:04 PM
bizman's avatar
BizMan
It's going to be a huge success!
8:49:18 PM
bizman's avatar
BizMan
Speaking of which, why is the comedian bot repeating the same joke for 15 seconds?
8:52:55 PM
devbeard96's avatar
the_beard
Oh...well the thing is that we had to limit how often the bot would request jokes from the API.
8:54:11 PM
bizman's avatar
BizMan
I'm not a developer, so please correct me if I'm wrong. From what I understand we're using a database to store jokes. Is there a limit on that too?
8:58:29 PM
devbeard96's avatar
the_beard
Well....not really. We can probably figure out a way to only cache API requests and not our own jokes.
8:59:52 PM
bizman's avatar
BizMan
If you could do that ASAP that would be great. Thanks.
9:01:05 PM
+

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
Giving the parent class the responsibility for its children removes flexibility, making future changes more difficult.

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
Making assumptions about the future is risky.

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
Single Responsibility Principle can be used as an easy mess detector.

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:

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
Simply moving code away in a new method is just hiding the complexity without dealing with the problem.

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
It's still just tight coupling, but part of the responsibility now has moved to children.

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
Introducing more change is going to scale poorly.

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 joke
dadBot.setContentBehavior(new Comedian_Joke());
// You could also override the `getContent` method directly
dadBot.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.

More Patterns?

Comments? Suggestions? Or maybe you just want to be kept in the loop when the next post is published? Get in touch on Twitter:

(END)