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.
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.
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.
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!