Strategy Pattern: Encapsulating Actions

Introduction

Very often when writing code you hack some implementation of some business logic together and find yourself needing to either modify the code, reuse it code somewhere else, or change the implementation all-together. When facing these situations, it’s important to recognize this as an opportunity to encapsulate what varies and insulate your application code from further change.

Enter the strategy pattern. Strategy defines a set of algorithms with a common interface and encapsulates their implementations, thus making them interchangeable and decoupled from the clients that use them.

The main goal is to split out actions from objects so that the actions can be changed or reused. By leveraging the strategy pattern along with setter injection, you can even change behavior at runtime.

Example

A default implementation without the strategy pattern would look something like this:

const paypal = require('paypal');

class OrderController {
  constructor() {
    this.create = this.create.bind(this);
  }
  create(req, res) {
    // validate and do ordery things
    paypal.charge(data, (err, res) => {
      res.status(200);
      res.send();
    });
  }
}

While this code may work perfectly fine, there are two main issues that will prevent it from standing the test of time.

  • Hardcoded implementation
  • High Coupling – The order controller is tightly coupled to a particular payment provider implementation (in this case, PayPal). Assuming the controller does more than just ‘charge’, any change in a payment processor would probably require a complete refactoring
  • Low Cohesion – The code is hard to read; there’s validation, order-specific logic, and payment specific logic in one big method. If they each contain a couple of async calls, you’re likely dealing with a pyramid of doom
  • Redundancy
  • The payment provider is being hardcoded directly into client code along with its implementation. If you wanted to create a different type of order or some other application widget that also charged something; you’ll probably have to re-write all of this logic since it’s embedded directly into the order functions
  • If you ever want to change payment providers, you can rest assured knowing that you’ll have multiple areas of code to update at this point because every method has its own references to a hardcoded implementation

Example

An alternative approach to solving this problem, leveraging the payment provider as a strategy:

const StripeService = { charge() {} };
const PapyalService = { charge() {} };

// this can be reused
class PaymentService {
  constructor(paymentProvider) {
    // provider is abstracted
    this.provider = paymentProvider;
    this.changeProvider = this.changeProvider.bind(this);
    this.charge = this.charge.bind(this);
  }
  changeProvider(paymentProvider) {
    this.paymentProvider = paymentProvider;
  }
  // doesn't know or care which provider it's charging
  charge() {
    this.paymentProvider.charge();
  }
}

// not tied to a particular processor
class OrderController {
  constructor(paymentService) {
    this.paymentService = paymentService;
    this.create = this.create.bind(this);
  }
  create(req, res) {
    // validate and do ordery things
    // doesn't know which provider is being used
    this.paymentService.charge(data, (err, res) => {
      res.status(200);
      res.send();
    });
  }
}

Splitting out your payment code (or any non-particular actions for that matter) enabled the code to be reused and decreases the likelihood that your calling code will need to be modified should some change be required within the underlying algorithm.

Use case

When dealing with strategy abstractions I typically find their usefulness to be within the infrastructure layer or when dealing with third-party integrations. Typically more robust wrappers in the form of services or repositories are required for more complex situations and a strategy pattern is really meant for an individual function or call. For a more robust re-usability solution, see Dependency Injection.

Thank you for reading! To be the first notified when I publish a new article, sign up for my mailing list!

Ben Lugavere is a Lead Engineer and Architect at Boxed where he develops primarily using JavaScript and TypeScript. Ben writes about all things JavaScript, system design and architecture and can be found on twitter at @benlugavere.

Follow me on medium for more articles or on GitHub to see my open source contributions!