DEV Community

loading...
Cover image for Functional Programming Design Patterns: Part 1 - Strategy Pattern

Functional Programming Design Patterns: Part 1 - Strategy Pattern

qgabe profile image Gabriel Ong ・4 min read

This post is Part 1 of a series on utilizing Design Patterns in the context of Functional Programming. If you are interested in learning more, stay tuned for more on my DEV page.

Introduction

Made ubiquitous by the indispensable book Design Patterns: Elements of Reusable Object-Oriented Software written by the Gang of Four, Design Patterns in Software Engineering (SE) are often utilized as battle-tested ways to solve recurrent system design problems in a flexible and reusable way.


Design Patterns: Elements of Reusable Object-Oriented Software

The now must-have book in every Software Engineer's bookshelf. [source]

However, Design Patterns are more often than not, are utilized in an Object-Oriented Programming (OOP) fashion. Many patterns include the use of abstract classes, interfaces and other OOP features that do not make sense in the context of Functional Programming (FP), where whole systems are composed of functions instead of concrete classes.

Thus, this poses a sort of cognitive mismatch for many engineers who have internalized these OOP patterns and are finding themselves in an environment where FP is growing in popularity.

This post and the subsequent series seeks to provide some instruction and examples on using some of these patterns in the context of FP.

The Strategy Pattern

The Strategy Pattern from the GoF book is described as a way to vary a family of encapsulated algorithms to make them interchangeable from a runtime context. That definition can be a mouthful, so let's break it down.

Strategy

We have a few components here:

  • Algorithm - A procedure that takes some value as input, performs some steps on the input, and produces an output.
  • Strategy - A common interface implemented by all Algorithms.
  • Context - A Run-time environment or a parent process that seeks to utilize a different algorithm given some condition.

Retail Store Example

To make it easier to understand this, let us adapt this pattern in a real-life context:

Suppose we have a retail store and we want to introduce a way to implement different pricing algorithms to calculate the total price of a customer's order depending on the type of a customer (walk-in or online).

We can first define our pricing algorithms as follows:

Strategy and Algorithms

interface PricingStrategy {
    public float getTotalPrice(CustomerOrder);
}

class WalkInPricing implements PricingStrategy {
  public float getTotalPrice(CustomerOrder order) {
      // Calculate the price per item in order
      // according to the walk-in pricing
      ...
      return totalPrice;
  }
}

class OnlinePricing implements PricingStrategy {
  public float getTotalPrice(CustomerOrder order) {
      // Calculate the price per item in order
      // according to the online pricing
      ...
      return totalPrice;  
  }
}
Enter fullscreen mode Exit fullscreen mode

Following that, we can then introduce the strategy and algorithms to the context, like so:

Context

PricingStrategy pricing;
...
// First, we decide type of customer and pricing algorithm to use.
switch(customer.type) {
    case "walkIn":
      this.pricing = new WalkInPricing();
      break;
    case "online":
      this.pricing = new OnlinePricing();
      break;
    default:
      throw new IllegalCustomerType("...");
}

// Secondly, we get the calculated total price for the order using the pricing algorithm.
this.totalPrice = this.pricing.getTotalPrice(customer.order);
...
Enter fullscreen mode Exit fullscreen mode

This is pretty simple and intuitive!

There are a few benefits to designing the variation this way:

  1. We avoid implementing a single brittle monolithic algorithm that accounts for all the conditions. Or even implementing the logic in Context itself! (e.g. Implementing a PricingAlgorithm class that has a lot of if/else and switch statements to check the customer type).
  2. We introduce extensibility by encapsulating the algorithms separately. If we want to introduce a new pricing algorithm for a new customer type, we simply need to extend the switch statement in Context and have the new pricing algorithm implement the Strategy interface.

The Functional Approach

Now that we've established how to use the Strategy Pattern in an OOP fashion, how do we implement the same kind of logic in a FP-friendly way?

Well, quite simply we can leverage the idea of higher-order functions, where functions can take in other functions as parameters.

const getMathResult = (mathOperation, arguments) => {
    return mathOperation(arguments);
};
Enter fullscreen mode Exit fullscreen mode
A simple higher-order function.

This allows us to represent a similar abstraction provided by using an interface in OOP!

Let us explore how we can do this:

const walkInPricing = (orders: CustomerOrder): Number => {
    // Calculate the price per item in order
    // according to the walk-in pricing
    ...
    return totalPrice;
}

const onlinePricing = (orders: CustomerOrder): Number => {
    // Calculate the price per item in order
    // according to the online pricing
    ...
    return totalPrice;      
}


const getTotalPrice =
(pricingStrategy: (CustomerOrder) => Number,
orders: CustomerOrders): Number => {
    return pricingStrategy(orders);
}

...
let pricing;
switch(customer.type) {
    case "walkIn":
      pricing = walkInPricing;
      break;
    case "online":
      pricing = onlinePricing;
      break;
    default:
      throw new IllegalCustomerType("...");
}
getTotalPrice(pricing, orders);
Enter fullscreen mode Exit fullscreen mode

As you can see, we simply substitute the Strategy interface with the getTotalPrice function which consumes the pricing algorithm specified as a parameter and returns us the price for the given function.

This structure allows us to leverage off the same extensibility and encapsulated design that the pattern gives us in the OOP approach! With lesser OOP specific syntax, the code is also shorter and more understandable, which more complex implementations can benefit from.

What's next

This post is Part 1 of a series on utilizing Design Patterns in the context of Functional Programming. If you are interested in learning more about design patterns implementing in the Functional Programming paradigm, stay tuned for more on my DEV page.

Discussion

pic
Editor guide