Introduction
In software and web development, it's always important to write code that is maintainable and extendable. The solution that you first create will likely change over time. So, you need to make sure you write your code in a way that doesn't require a whole rewrite or refactor in the future.
The strategy pattern can be used to improve the extendability of your code and also improve the maintainability over time.
Intended Audience
This post is written for Laravel developers who have an understanding of how interfaces work and how to use them to decouple your code. If you're a little unsure about this subject, check out my post from last week that discusses using interfaces to write better PHP code.
It's also strongly advised that you have an understanding of dependency injection and how the Laravel service container works.
What Is the Strategy Pattern?
Refactoring Guru defines the strategy pattern as a "behavioral design pattern that lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable.". This might sound a bit scary at first, but I promise that it's not as bad as you think. If you want to read more into design patterns, I'd highly recommend checking out Refactoring Guru. They do a great job of explaining the strategy pattern in depth as well as other structural patterns.
The strategy pattern is basically a pattern that helps us to decouple our code and make it super extendable.
Using the Strategy Pattern in Laravel
Now that we have a basic idea of what the strategy pattern is, let's look at how we can use it ourselves in our own Laravel application.
Let's imagine that we have a Laravel application that users can use for getting exchange rates and currency conversions. Now, let's say that our app uses an external API (exchangeratesapi.io) for getting the latest currency conversions.
We could create this class for interacting with the API:
1class ExchangeRatesApiIO2{3 public function getRate(string $from, string $to): float4 {5 // Make a call to the exchangeratesapi.io API here and fetch the exchange rate.6 7 return $rate;8 }9}
Now, let's use this class in a controller method so that we can return the exchange rate for a given currency. We're going to use dependency injection to resolve the class from the container:
1class RateController extends Controller 2{ 3 public function __invoke(ExchangeRatesApiIO $exchangeRatesApiIO): JsonResponse 4 { 5 $rate = $exchangeRatesApiIO->getRate( 6 request()->from, 7 request()->to, 8 ); 9 10 return response()->json(['rate' => $rate]);11 }12}
This code will work as expected, but we've tightly coupled the ExchangeRatesApiIO
class to the controller method. This means that if we decide to migrate over to using a different API, such as Fixer, in the future, we'll need to replace everywhere in the codebase that uses the ExchangeRatesApiIO
class with our new class. As you can imagine, in large projects, this can be a slow and tedious task sometimes. So, to avoid this issue, instead of trying to instantiate a class in the controller method, we can use the strategy pattern to bind and resolve an interface instead.
Let's start by creating a new ExchangeRatesService
interface:
1interface ExchangeRatesService2{3 public function getRate(string $from, string $to): float;4}
We can now update our ExchangeRatesApiIO
class to implement this interface:
1class ExchangeRatesApiIO implements ExchangeRatesService2{3 public function getRate(string $from, string $to): float4 {5 // Make a call to the exchangeratesapi.io API here and fetch the exchange rate.6 7 return $rate;8 }9}
Now that we've done that, we can update our controller method to inject the interface rather than the class:
1class RateController extends Controller 2{ 3 public function __invoke(ExchangeRatesService $exchangeRatesService): JsonResponse 4 { 5 $rate = $exchangeRatesService->getRate( 6 request()->from, 7 request()->to, 8 ); 9 10 return response()->json(['rate' => $rate]);11 }12}
Of course, we can't instantiate an interface; we want to instantiate the ExchangeRatesApiIO
class. So, we need to tell Laravel what to do whenever we try and resolve the interface from the container. We can do this by using a service provider. Some people prefer to keep things like this inside their AppServiceProvider
and keep all of their bindings in one place. However, I prefer to create a separate provider for each binding that I want to create. It's purely down to personal preference and whatever you feel fits your workflow more. For this example, we're going to create our own service provider.
Let's create a new service provider using the Artisan command:
1php artisan make:provider ExchangeRatesServiceProvider
We'll then need to remember to register this service provider inside the app/config.php
like below:
1return [2 3 'providers' => [4 // ...5 \App\Providers\ExchangeRatesServiceProvider::class,6 // ...7 ],8 9]
Now, we can add our code to the service provider to bind the interfaces and class:
1class ExchangeRatesServiceProvider extends ServiceProvider2{3 public function register(): void4 {5 $this->app->bind(ExchangeRatesService::class, ExchangeRatesApiIO::class);6 }7}
Now that we've done all of this, when we dependency inject the ExchangeRatesService
interface in our controller method, we'll receive an ExchangeRatesApiIO
class that we can use.
Taking It Further
Now that we know how to bind an interface to a class, let's take things a bit further. Let's imagine that we want to be able to decide whether to use the ExchangeRatesAPI.io or the Fixer.io API whenever we'd like just by updating a config field.
We don't have a class yet for dealing with the Fixer.io API yet, so let's create one and make sure that it implements the ExchangeRatesService
interface:
1class FixerIO implements ExchangeRatesService2{3 public function getRate(string $from, string $to): float4 {5 // Make a call to the Fixer API here and fetch the exchange rate.6 7 return $rate;8 }9}
We'll now create a new field in our config/services.php
file:
1return [2 3 //...4 5 'exchange-rates-driver' => env('EXCHANGE_RATES_DRIVER'),6 7];
We can now update our service provider to change which class will be returned whenever we resolve the interface from the container:
1class ExchangeRatesServiceProvider extends ServiceProvider 2{ 3 public function register(): void 4 { 5 $this->app->bind(ExchangeRatesService::class, function ($app) { 6 if (config('services.exchange-rates-driver') === 'exchangeratesapiio') { 7 return new ExchangeRatesApiIO(); 8 } 9 10 if (config('services.exchange-rates-driver') === 'fixerio') {11 return new FixerIO();12 }13 14 throw new Exception('The exchange rates driver is invalid.');15 });16 }17}
Now if we set our exchanges rates driver in our .env to EXCHANGE_RATES_DRIVER=exchangeratesapiio
and try to resolve the ExchangeRatesService
from the container, we will receive an ExchangeRatesApiIO
class. If we set our exchanges rates driver in our .env to EXCHANGE_RATES_DRIVER=fixerio
and try to resolve the ExchangeRatesService
from the container, we will receive an FixerIO
class. If we set driver to anything else accidentally, an exception will be thrown to let us know that it's incorrect.
Due to the fact that both of the classes implement the same interface, we can seamlessly change the EXCHANGE_RATES_DRIVER
field in the .env file and not need to change any other code anywhere.
Conclusion
Is your brain fried yet? If it is, don't worry! Personally, I found this topic pretty difficult to understand when I first learnt about it. I don't think I started to really understand it until I put it into practice and used it myself. So, I'd advise spending a little bit of time experimenting with this yourself. Once you get comfortable with using it, I guarantee that you'll start using it in your own projects.
Hopefully, this article has given you an overview of what the strategy pattern is and how you can use it in Laravel to improve the extendability and maintainability of your code.
If you found this post useful, I'd love to hear about it in the comments.
Keep on building awesome things! 🚀