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 "behavioural 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. In fact, you probably use it every day with Laravel without even noticing whenever you use the Storage
and Cache
facades (and a few other places too). Let's say that you use this code:
Cache::put('name', 'Ash Allen', 600);
The code above is resolving a class from the service container by using the facade. I won't get into how facades work as it's not really the purpose of this article, but the important bit to know is that the Cache
facade here is binding an interface (Illuminate\Contracts\Cache\Factory
to be specific) to a class and using it. It is then storing the word 'Ash Allen' under the key 'name' for 10 minutes.
As you'll have probably noticed in the Laravel documentation and in your project's config, Laravel supports a few different drivers for caching, including: Redis, DynamoDB, Memcached and the database. So, for example, if we were to set our cache driver in our .env file to CACHE_DRIVER=redis
, when we run our code snippet above, the data would be stored in our Redis cache. However, if we were to change the driver to be CACHE_DRIVER=database
, this would result in data being stored in the database instead.
Each different cache driver has it's own respective class that deals with how the framework interacts with the cache. So, when we change the driver in our .env, Laravel needs to know which driver to use. This is where the strategy pattern steps in. Under the hood, whenever we use the Cache
facade, Laravel is actually resolving an Illuminate\Contracts\Cache\Factory
interface from the service container. It does this by checking the config value (e.g. redis, database, etc.) and then mapping that interface to a class. For example, whenever we have our cache driver set to CACHE_DRIVER=redis
and we try to resolve the Factory
interface, we will get a class that specifically interacts with the Redis cache and nothing else.
As you can see, the strategy pattern can improve the extendability of your code. For example, if we wanted to create our own custom cache driver, we could just create the implementation and then let Laravel know that it is available for using. For a bit more context, check out the Laravel documentation to see an example of how you can add your own driver.
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:
class ExchangeRatesApiIO
{
public function getRate(string $from, string $to): float
{
// Make a call to the exchangeratesapi.io API here and fetch the exchange rate.
return $rate;
}
}
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:
class RateController extends Controller
{
public function __invoke(ExchangeRatesApiIO $exchangeRatesApiIO): JsonResponse
{
$rate = $exchangeRatesApiIO->getRate(
request()->from,
request()->to,
);
return response()->json(['rate' => $rate]);
}
}
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:
interface ExchangeRatesService
{
public function getRate(string $from, string $to): float;
}
We can now update our ExchangeRatesApiIO
class to implement this interface:
class ExchangeRatesApiIO implements ExchangeRatesService
{
public function getRate(string $from, string $to): float
{
// Make a call to the exchangeratesapi.io API here and fetch the exchange rate.
return $rate;
}
}
Now that we've done that, we can update our controller method to inject the interface rather than the class:
class RateController extends Controller
{
public function __invoke(ExchangeRatesService $exchangeRatesService): JsonResponse
{
$rate = $exchangeRatesService->getRate(
request()->from,
request()->to,
);
return response()->json(['rate' => $rate]);
}
}
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:
php artisan make:provider ExchangeRatesServiceProvider
We'll then need to remember to register this service provider inside the app/config.php
like below:
return [
'providers' => [
// ...
\App\Providers\ExchangeRatesServiceProvider::class,
// ...
],
]
Now, we can add our code to the service provider to bind the interfaces and class:
class ExchangeRatesServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(ExchangeRatesService::class, ExchangeRatesApiIO::class);
}
}
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:
class FixerIO implements ExchangeRatesService
{
public function getRate(string $from, string $to): float
{
// Make a call to the Fixer API here and fetch the exchange rate.
return $rate;
}
}
We'll now create a new field in our config/services.php
file:
return [
//...
'exchange-rates-driver' => env('EXCHANGE_RATES_DRIVER'),
];
We can now update our service provider to change which class will be returned whenever we resolve the interface from the container:
class ExchangeRatesServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(ExchangeRatesService::class, function ($app) {
if (config('services.exchange-rates-driver') === 'exchangeratesapiio') {
return new ExchangeRatesApiIO();
}
if (config('services.exchange-rates-driver') === 'fixerio') {
return new FixerIO();
}
throw new Exception('The exchange rates driver is invalid.');
});
}
}
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! 🚀
Top comments (6)
Would it be better practice to remove the selection logic from the ExchangeRatesServiceProvider class and move it into an ExchangeRatesProviderFactory class, just having the service provider instantiate the factory and then allow the factory to determine what to return?
Hi Jason, that's a very good question. To be totally honest, I'm not sure which of them is the better practice. But I think that both approaches would be more or less doing the same thing, so my initial guess would be that it would be up to personal preference 😄
Thanks a lot for your for your explanation, It was so helpful.
It's no problem at all, I'm glad that you found it useful!
Great post! This pattern is amazing! I'll study more about this one, but your post was helpful!
Thanks, I'm glad that you found it useful! And yeah I definitely think that it's a useful pattern and can be extremely powerful if used in the right places!