Strategy Pattern in JavaScript
In this article we will describe the Strategy Pattern. We will define a real world scenario and explain how to solve it using the Strategy Design Pattern approach.
According to Wikipedia: in computer programming, the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
Real World Scenario and Strategy Pattern Class Diagram
Let’s look at the scenario below:
In this scenario we have a Warehouse Management System, WMS. When order is shipped, meaning, tracking information is entered into the system, the system needs to check order’s source and send the tracking information to the appropriate destination. If order comes from Shopify ecommerce platform, the system needs to send tracking info to Shopify. If it is BigCommerce order, the tracking needs to be sent to BigCommerce. Otherwise, the system needs an email to the customer on the order that their purchase is on the way.
Take your Node.js skills to the next level with Node.js Design Patterns! Master best practices for building production-grade applications and scalable architectures. Start transforming your development today! 👉 https://amzn.to/3N19LvV
Let’s look at the Strategy Pattern implementation diagram:
At the top of the inheritance tree there’s an abstract class TrackingInfoSender
It is extended by children: ShopifyTrackingInfoSender
, BigCommerceTrackingInfoSender
, and EmailTrackingInfoSender
. TrackingInfoSenderClass
has an abstract method sendTracking
. Since it is an abstract method TrackingInfoSender
class also works as an interface, meaning all children have to implement sendTrackingMethod
. Sometimes an advantage of having an abstract parent class instead of a simple interface is that duplicate logic of child classes can be pushed up to the parent class enforcing DRY principle. In our case each child class has assign a corresponding order to order property. This logic is handled by the parent.
Another method of the TrackingInfoSender
class is getSender()
. This is a static factory method. It will decided which child tracking info sender class to instantiate depending on the order source.
Strategy Pattern Implementation
Now let’s translate this diagram into code. If you prefer, you can also watch this video on how to implement the Strategy Pattern.
We will be working in a Node.js project configured with TypeScript, ESLint, and Prettier.
Let’s create order.ts
file in strategyPattern
folder.
export enum OrderSource { Shopify = "shopify", BigCommerce = "bigcommerce", Internal = "internal", } export class Order { source: OrderSource; constructor(source: OrderSource) { this.source = source; } }
This code defines Order class. There’s only one property on this class – source
. However, in real life there will be many more properties. For the sake of our discussion we have to keep things as simple as possible.
Next, let’s create trackingInfo.ts
file in the same folder and put the following code in it.
import { Order, OrderSource } from "./order.js"; export default abstract class TrackingInfoSender { protected order: Order; constructor(order: Order) { this.order = order; } abstract sendTracking(): void; static getSender(order: Order): TrackingInfoSender { let sender = new EmailTrackingInfoSender(order); switch (order.source) { case OrderSource.Shopify: sender = new ShopifyTrackingInfoSender(order); break; case OrderSource.BigCommerce: sender = new BigCommerceTrackingInfoSender(order); break; default: sender = new EmailTrackingInfoSender(order); } return sender; } } class ShopifyTrackingInfoSender extends TrackingInfoSender { constructor(order: Order) { super(order); } sendTracking(): void { console.log("Sending tracking information to Shopify"); } } class BigCommerceTrackingInfoSender extends TrackingInfoSender { constructor(order: Order) { super(order); } sendTracking(): void { console.log("Sending tracking information to BigCommerce"); } } class EmailTrackingInfoSender extends TrackingInfoSender { constructor(order: Order) { super(order); } sendTracking(): void { console.log("Emailing tracking information to the customer"); } }
This code exports TrackingInfoSender
abstract class. The class has a static factory method getSender
that decides which concrete tracking info sender class to instantiate depending on order source. Each of tracking info sender strategy classes extend TrackingInfoSender
and implement sendTracking
method. This way we ensure that sendTracking
method can be called on any strategy class. Another advantage of having a parent class is that logic common to all tracking info sender classes can be implemented in TrackingInfoSender
parent class, thus making sure the DRY principle is followed.
Finally, let’s create trackingInfo.ts
in strategyPattern
folder and put everything together.
import { Order, OrderSource } from "./order.js"; import TrackingInfoSender from "./trackingInfo.js"; const orders = [ new Order(OrderSource.Shopify), new Order(OrderSource.Internal), ]; const trackingInfoSenders = orders.map((order) => TrackingInfoSender.getSender(order) ); function sendTrackingInfo(trackingInfoSender: TrackingInfoSender) { trackingInfoSender.sendTracking(); } export default function runStrategyPattern() { trackingInfoSenders.forEach((sender) => sendTrackingInfo(sender)); }
First we create an array of orders – one order is from Shopify and another order is an internal one. We map over the array and get tracking number info sender class for each of the orders. getSender
factory method will determine which strategy will be applied when sending tracking data. After that we define function sendTrackingInfo
. It takes trackingInfoSender
as a parameter. Since tracking info sender classes extend TrackingInfoSender
, we can type trackingInfoSender
parameter as TrackingInfoSender
class. Finally runStrategyPattern
function calls forEach
method on the array of trackingInfoSenders
and calls sendTrackingInfo
function on each info sender.
Now we can import runStrategyPattern
function in index.ts
file in src
folder and run it.
Conclusion
To summarize: We first defined each strategy as its own class. Then we created a parent class that has a factory method that decides which strategy to use by instantiating an appropriate class. The parent class also has an abstract method that each child strategy class has to implement. This ensures we can type strategy classes using the parent class and call that method on any of the classes.
In conclusion, the Strategy Pattern proves to be a powerful ally in JavaScript development, providing a structured approach to code organization and enhancing flexibility. By applying this design pattern, you empower your projects with a scalable and maintainable architecture. As you navigate the dynamic landscape of JavaScript, consider the Strategy Pattern as a valuable tool to achieve code excellence. Happy coding!