Introduction
As you know, Design Patterns are special solutions designed by experts for common problems. They are not a ready-to-use library or a package, conversely, they are general concepts you can follow to solve your problem regardless of the programming language you use.
There are three major categories for these patterns which are Creational, Structural, and Behavioral Design Patterns.
In this post, we will know the differences between three of so close Behavioral Design Patterns which are Strategy, State , and Template.
We will take the same example to just define the differences easily not to decide that this pattern is the perfect fit for this situation.
Let's imagine a scenario in which a user can choose from different payment gateways to pay for his order or the payment gateway is predefined in the API.
Strategy
In Strategy , you can define a family of strategies (algorithms) that can be used interchangeably to solve a specific problem.
As you can see, if we want to add a new payment gateway, all we have to do is just adding a new strategy implementing this gateway logic which respects the Open-Closed Principle perfectly.
Let's jump right into our example:
interface IGateway {
gatewayAuth(): void
pay(): void
}
// Strategies
class Gateway1 implements IGateway {
gatewayAuth() { console.log('Gateway1 => gatewayAuth()'); }
pay() { console.log('Gateway1 => pay()'); }
}
class Gateway2 implements IGateway {
gatewayAuth() { console.log('Gateway2 => gatewayAuth()'); }
pay() { console.log('Gateway2 => pay()'); }
}
// Context
class Order {
constructor(private gateway: IGateway) {} // Strategy injection at runtime
private createOrder() { console.log('Order => createOrder()'); }
pocessOrder() {
if (!this.gateway) { throw new Error('No gateway!'); }
this.gateway.gatewayAuth();
this.gateway.pay();
this.createOrder();
}
}
// Client
const gateway2 = new Gateway2();
// You can change the entire algorithm by injecting another strategy
const order = new Order(gateway2);
order.pocessOrder();
From the previous example, we note that the Strategy pattern lets you change the Order (Context) object behavior at the runtime by injecting a specific payment gateway and its behavior will remain unchanged for the rest of the lifespan of the Order (Context) object.
When should you use the Strategy pattern?
Simply, when you have many variants (strategies) which can be used interchangeably and you want to switch between them just at the runtime (at the Context creation time).
If your Context object has many if...else
statements, this might be an indicator to replacing them with Strategies.
State
The State pattern is a specialization of the Strategy pattern and is so close to it.
As you can see, the State pattern almost looks like the Strategy pattern. And as Strategy , if we want to add a new payment gateway, all we have to do is adding a new State which also respects the OCP.
Let's jump right into our example:
interface IGateway {
gatewayAuth(): void
pay(): void
}
// States
class Gateway1 implements IGateway {
gatewayAuth() { console.log('Gateway1 => gatewayAuth()'); }
pay() { console.log('Gateway1 => pay()'); }
}
class Gateway2 implements IGateway {
gatewayAuth() { console.log('Gateway2 => gatewayAuth()'); }
pay() { console.log('Gateway2 => pay()'); }
}
// Context
class Order {
private currentGateway!: IGateway;
private gateways: { [key: string]: IGateway } = {
gateway1: new Gateway1(),
gateway2: new Gateway2()
}
constructor() {
this.changeGateway('gateway1'); // Defualt State
}
// State transition
changeGateway(gatewayName: string) {
this.currentGateway = this.gateways[gatewayName];
}
private createOrder() { console.log('Order => createOrder()'); }
pocessOrder() {
if (!this.currentGateway) { throw new Error('No gateway!'); }
this.currentGateway.gatewayAuth();
this.currentGateway.pay();
this.createOrder();
}
}
// Client
const order = new Order();
order.changeGateway('gateway2');
order.pocessOrder();
// You can switch between States dynamically during the lifespan of the Order object
order.changeGateway('gateway1');
order.pocessOrder();
From the previous example, you may note here you can switch between the payment gateways ( States ) dynamically for the rest of the lifespan of the Order (Context) object.
Where should you implement the State transition?
- Client : As we do in the previous example which decreases the coupling, but unfortunately this isn’t the case all the time.
- Context : In this case, the Context will know about all the possible states and how to transition between them which increases the coupling.
- State : Every state can transition between each other which increases the coupling as well between the states.
So what is the difference between Strategy and State?
- In the Strategy pattern, the Context behavior changes once by injecting a specific strategy and remains unchanged for the rest of its lifespan . Conversely, in the State pattern, the Context behavior can be changed dynamically during the rest of its lifespan by switching between its States.
- In the State pattern, if the state transition occurs inside each state then every state will know about each other, in contrary in the Strategy pattern, strategies don’t know anything about each other.
When should you use the State pattern?
Simply, when your object ( Context ) has many states and changes its behavior frequently depending on these states during the rest of its lifespan.
Like Strategy , if your Context object has many if...else
statements, this might be an indicator to use the State pattern.
Template
The Template pattern defines the skeleton of an algorithm in the superclass and overrides specific steps of this algorithm by its subclasses without changing its structure.
From the diagram, we note that the superclass (Order) lets its subclasses override specific tasks of its algorithm. As a result, if we want to add a new payment gateway all we have to do is adding a subclass that overrides these specific tasks.
So what is the difference between Strategy and Template?
- Strategy varies the entire algorithm via its strategies.
- Template varies specific parts of the algorithm by subclasses.
Let's jump right into our example:
// Superclass
abstract class Order {
// Specific parts of the algorithm will be overridden by subclasses
abstract gatewayAuth(): void
abstract pay(): void
// While other parts remain unchanged
private createOrder() { console.log('Order createOrder()'); }
// The algorithm skeleton
processOrder() {
this.gatewayAuth();
this.pay();
this.createOrder();
}
}
// Subclasses (Override specific parts of the algorithm)
class Gateway1 extends Order {
gatewayAuth() { console.log('Gateway1 => gatewayAuth()'); }
pay() { console.log('Gateway1 => pay()'); }
}
class Gateway2 extends Order {
gatewayAuth() { console.log('Gateway2 => gatewayAuth()'); }
pay() { console.log('Gateway2 => pay()'); }
}
// Client
const gateway2 = new Gateway2();
// Subclass is responsible to run the main algorithm
gateway2.processOrder();
From the previous example, we now deal directly with the subclasses which are responsible to run the template method (processOrder).
When should you use the Template pattern?
You should use the Template pattern if you have an algorithm and you want to override specific steps (not the entire algorithm) of it with interchangeable subclasses.
Conclusion
So, what is the best pattern? as always, it depends, it is up to the developer who should understand the general concepts and the pros and cons of each pattern and as a result, decides what is better for his situation.
Resources
Nodejs Design Patterns 3rd edition