Decision making using a strategy

August 2020 | Permalink

Illustration by Barbara

Developing software through code mostly means applying well estabilished design patterns. This is no different then choosing the most popular recipe to cook a beef Wellington:

  • You want to rely on battle-tested experience
  • You want to make sure the process is streamlined
  • You want to minimise disruption and the risk of failure
  • You want guaranteed results

Design patterns are recipe frameworks: problems that have already been solved successfully and efficiently in the past do not require a new solution. Design patterns in fact, are the foundation on top of which all software is built.

I will not go through the different types of software design patterns (some of which I honestly have very little knowledge of), but I would like to focus on a particular behavioural pattern that I found myself committing to very often while developing frontend applications.

The Strategy Pattern

The Strategy Pattern can be defined as a class of implementations that can be chosen at run-time by a context class. This definition seems to refer to an object-oriented architecture however, we will see how elements of strategy patterns can be applied in a functional way as well.

So how does it work in practice?

We want our customers app to have a "contact" functionality and we are expected to use their preferred contact method (email or phone for example) — The Business

In this scenario, you might have a customer representation that looks like this:

class Customer {
    private id: string;
    public name: string;
    public email: string;
    public phoneNumber: string;
    public address: string;

    public contact() {
        CustomerPhone.dial(this.phoneNumber);
    }
}

This approach allows the software to invoke the contact method and utilise the underlying functionality: a CustomerPhone class method to dial a number.

How does that cater for cases where the customer prefers to be contacted by email? It does not. Nothing stops us from discriminating the functionality within the method though:

public contact() {
    if (this.prefersEmails) {
        CustomerEmail.send(this.email);
    } else {
        CustomerPhone.dial(this.phoneNumber);
    }
}

There's nothing inherently wrong with this approach, however you might already guess what could go south if more functionality depends on a customer's contact preference.

A customer who prefers emails will be subscribed to our monthly newsletter, otherwise we will send them a catalogue by post. — The Business

Let us see how this could be translated into a Customer class method:

public sendMarketingMaterial() {
    if (this.prefersEmails) {
        CustomerNewsletter.send(this.email);
    } else {
        CustomerCatalogue.post(this.address);
    }  
}

It is self evident that our Customer class is being polluted from these contact preference checks and that could result in maintenance hardship down the road.

A strategy pattern in this scenario will encapsulate the contact preference check into a system where this choice is made once and a whole package of behaviours is derived from it. It starts with an interface that defines which behaviours of a Customer class we intend to target:

interface CustomerContactStrategy {
    contact: (customer: Customer) => void;
}

For each behaviour we intend to implement, we define a class that must implement CustomerContactStrategy:

class CustomerPhoneContactStrategy implements CustomerContactStrategy {
    public contact(customer: Customer) {
        CustomerPhone.dial(customer.phoneNumber);
    }
}

class CustomerEmailContactStrategy implements CustomerContactStrategy {
    public contact(customer: Customer) {
        CustomerEmail.send(customer.email);
    }
}

Once the strategies are defined, we need to transform our Customer class into a strategy context class (a class that supports a strategy):

class Customer {
    //...
    private strategy: CustomerContactStrategy;

    public setStrategy(strategy: CustomerContactStrategy) {
        this.strategy = strategy;
    }

    public contact() {
        this.strategy.contact(this);
    }
}

The Customer instance will now be able to hold a reference to a behaviour strategy (hence the name of behavioural pattern). By invoking the contact method, the underlying strategy will be able to complete the task in the desired way.

Choosing the right method at run-time can be as easy as a ternary operator:

const customer = new Customer();
customer.setStrategy(
    customerPrefersEmails ? 
    new CustomerEmailContactStrategy()
    :
    new CustomerPhoneContactStrategy()
);

customer.contact();

The decision node here lies on customerPrefersEmails, a boolean value. Upon more complex decision flows, this might be delegated to a different class altogether:

customer.setStrategy(CustomerContactStrategies.choose(customer));

The above code is an implementation of a factory pattern and demonstrates how adhering to a design pattern might be beneficial as the software complexity increases.

The Strategy pattern in functional programming

The strategy pattern, as designed above, can only be applied in an object oriented architecture however elements of strategy can also be applied to function composition.

Let us review our strategies using functions:

type CustomerContactMethod = (customer: Customer) => void;

const emailCustomer: CustomerContactMethod = (customer: Customer) =>
    CustomerEmail.send(customer.email);

const phoneCustomer: CustomerContactMethod = (customer: Customer) =>
    CustomerPhone.dial(customer.phoneNumber);

And compose behaviour:

const contactCustomerViaPreferredMethod = (
    customer: Customer, 
    method: CustomerContactMethod
) => () => method(customer);

const contactCustomerViaEmail = contactCustomerViaPreferredMethod(
    customer, 
    emailCustomer
);

const contactCustomerViaPhone = contactCustomerViaPreferredMethod(
    customer, 
    phoneCustomer
);

// ....
if (customerPrefersEmails) {
    contactCustomerViaEmail();
} else {
    contactCustomerViaPhone();
}

On more complex decision flows, using a tuple:

const [contact, sendMarketingMaterial] = getCustomerInteractionMethods(
    customer, 
    getCustomerPreferredContactMethod(customer)
);

contact();
sendMarketingMaterial();

In Conclusion

A class-based strategy pattern in frontend applications is quite unlikely these days however, mastering strategies through function composition will get you very far, especially using those frameworks which seem to have embraced a functional approach to components (ie: React Hooks, Vue Composition API).

Useful resources: