The Strategy Pattern

The Strategy Pattern

The Strategy Pattern is a behavioral software design pattern.

It enables selecting an algorithm at runtime instead of hardcoding only one possible solution.

We'll take a look at what it actually is and how it can help you to write better software.

The issue

Whenever you hard-code some very specific logic, without the ability to replace it on-demand, you're tying yourself to that specific logic.

No way to replace it, except changing code. And sometimes changing code is not what you want to do, at all.

An object-oriented example

Imagine a class which is responsible to handle the creation of reports. In this particular case, you want to send reports out and have hard-coded an algorithm to send them over to a printer.

class ReportService {
  createReportAndSend() {
    // ...
    // ...
    const result = sendReport(report);

    if (result.error != null) {
      throw new Error("Sending report failed");
    }
  }
  sendReport(report: Report): ReportSendResult {
    const printerAddress = getPrinterAddress();
    const serializedReport = serializeReport(report);
    return sendReportToPrinter(serializedReport);
  }
}

But what if you ever wanted to send a report by mail? What if you want to send a report to, let's say an API endpoint? What about FTP, what about...?

I think you see where this is going.

Solving the issue

By making your ReportService only depend on an interface, any user, on construction of the service, can provide an implementation themselves. This is how you break the tight coupling to a certain algorithm.

interface ReportSender {
  sendReport(report: Report): ReportSendResult;
}

class ReportService {
  private readonly reportSender: ReportSender;

  constructor(reportSender: ReportSender) {
    this.reportSender = reportSender;
  }

  createReportAndSend() {
    // ...
    // ...
    const result = this.reportSender.sendReport(report);

    if (result.error != null) {
      throw new Error("Sending report failed");
    }
  }
}

Users can now:

  1. Provide an implementation on their own
  2. Use a third-party implementation
  3. Decide on which to use on-demand, from within their own code

Cross reference

As you might have already guessed, the strategy pattern plays well with The Open/Closed principle, known from SOLID.

By enabling a certain implementation to be passed in as a drop-in replacement, your logic can be extended from the outside!

Applying the pattern to functional programming

Even if you don't use classes, you can still apply the pattern to functional code you write.

functions can take other functions as parameters, and enable the user to choose a strategy by themselves, like below.

// hard coded logic
function processElements(array: any[]) {
  for (const value of array) {
    console.debug(array);
  }
}

// 🔽 Enabling users to choose a strategy by themselves 🔽

type Consumer = (element: any) => void;

function processElements(array: any[], processor: Consumer) {
  for (const value of array) {
    processor(value);
  }
}

Before you leave

If you like my content, visit me on Twitter, and perhaps you’ll like what you see!