Quick Design Patterns Series 1: The Strategy Pattern šŸŽÆ

Felipe Ɓlvarez
Mercadona Tech
Published in
5 min readMay 24, 2024

šŸ‘‹ Hi everyone! My name is Felipe Ɓlvarez, and I work as a Backend Engineer at Mercadona Tech. This is the first chapter of the Design Patterns series we want to write. You can see the rest of them here, in case you want šŸ˜‰

Soā€¦ letā€™s talk about the Strategy Pattern. Refactoring guru defines it like this:

Strategy is 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.

One of the reasons I find this pattern so compelling is its ability to illustrate the benefits of adhering to SOLID principles, particularly the practice of programming to interfaces rather than implementations.

In the end, all good programming practices are based on decoupling your code and reducing dependencies, and I canā€™t think of a better way to do that than inverting your dependencies by injecting them (and using interfaces for the contracts between your components).

This is also the main idea behind Hexagonal Architectureā€™s magic, and interfaces are the best way to separate components between different layers.

And this is exactly what the Strategy Pattern does. Let me explain it with an example of one of our use cases.

Letā€™s say that in our physical stores, our users (gerentes) receive trucks full of containers with products daily. Once a container has arrived at the store, they tell us (by using our Stocks application) that the container has arrived, so then we can add the content of that container (the products) to the storeā€™s stock. This action is called ā€œreceptionā€ of a container.

To perform that action, we take the container information and call a Mercadona IT service to perform that reception.

A simplified flow would be like this:

Physical container arrives at the store ā†’ The gerente scans the label of that container with the V6 ā†’ We take that container info and call a Mercadona IT service ā†’ They add the products of that container to the store stock ā†’ We confirm to the gerente that the container has been receptioned correctly

Okay, until now, everything seems easy, but here comes the problem: not all containers are received the same way. Letā€™s say that we have 2 different types of containers: normal containers and fish containers, and for each type of container, we need to call a different Mercadona IT service.

How can we solve this? Letā€™s see some code examples:

We have the following endpoint for receptions of a container:

POST /api/containers/some_uid/reception/

And then the view (the infrastructure entry point) for that endpoint:

class ReceptionContainerView(APIView):
def post(self, request, uid: str):
container = ContainerRepository().retrieve_by_uid(Uid(uid))

ReceptionContainer().execute(container)

return Response(status=200)

(Some of you have probably seen that we have a repository for accessing the database and a value object for the uid. We donā€™t use them exactly like that, but itā€™s easy to explain this, but letā€™s keep that thing for another post šŸ¤«)

And then the action:

class ReceptionContainer:
def execute(self, container: Container) -> None:
if container.is_fish():
return MercadonaFishContainerService().call(container.internal_id)

return MercadonaContainerService().call(container.internal_id)

As you can see, with this approach, our action is totally coupled with the implementation of the services (infrastructure), and we have a really ugly situation inside. What will happen if some of the implementations change? Or if now we have one more service to another type of container?

Soā€¦ Soā€¦ now itā€™s time to think of a solution and give it a cool refactor.

How about taking those external dependencies out of the action? Okay, we can create a class that encapsulates everything we need to call the external service. We can define a contract (interface) for this:

class ReceptionProvider(ABC):
@abstractmethod
def provide(container_id: str) -> None:
...

We have taken the convention of calling this type of class ā€œprovider,ā€ but you can use any other name like ā€œserviceā€ or whatever you want šŸ˜‰

So now we have our pretty interface, letā€™s inject it into our action:

class ReceptionContainer:
def __init__(self, reception_provider: ReceptionProvider):
this._reception_provider = reception_provider

def execute(self, container_id: str) -> None:
self._reception_provider.provide(container_id)

As you can see, our action is now totally decoupled with specific infra implementations. Thatā€™s nice!

What next? Letā€™s create the implementations for this interface:

class MercadonaReceptionProvider(ReceptionProvider):
def provide(container_id: str) -> None:
MercadonaContainerService().call(container_id)
class MercadonaFishReceptionProvider(ReceptionProvider):
def provide(container_id: str) -> None:
MercadonaFishContainerService().call(container_id)

And then inject one or another into our view:

class ReceptionContainerView(APIView):
def post(self, request, uid: str):
container = ContainerRepository().retrieve_by_uid(Uid(uid))

if container.is_fish():
ReceptionContainer(MercadonaFishReceptionService()).execute(container.internal_id)
return Response(status=200)

ReceptionContainer(MercadonaReceptionService()).execute(container.internal_id)
return Response(status=200)

And thatā€™s it! Here, we have applied our strategy pattern. But in reality, we like having our views and our actions as clean as possible, so what we usually do is put this logic inside the provider, and the final result would look like this (although with this, we are ā€œcorruptingā€ the pattern):

class ReceptionContainerView(APIView):
def post(self, request, uid: str):
ReceptionContainer(MercadonaReceptionProvider()).execute(Uid(uid))
return Response(status=200)
class ReceptionContainer:
def __init__(self, reception_provider: ReceptionProvider):
this._reception_provider = reception_provider

def execute(self, container_uid: Uid) -> None:
self._reception_provider.provide(container_uid)
class MercadonaReceptionProvider(ReceptionProvider):
def provide(container_uid: Uid) -> None:
container = ContainerRepository().retrieve_by_uid(uid)

if container.is_fish():
return MercadonaFishContainerService().call(container.internal_id)

return MercadonaContainerService().call(container.internal_id)

And now, whatā€™s the point of using a provider for this? Well, as you can see, in the end, we have been able to leave our view and our action clean, separating the code into classes that only do what they have to do. We have decoupled our action code from the specific implementations of the external services, and in the future, if something external changes, we just have to change our provider.

Typically, we inject the repo into the action, and the providerā€™s contract would be a Container (domain) object. This setup allows us to encapsulate the logic within the action, while the provider handles the external services. The overall structure would look like this:

class ReceptionContainerView(APIView):
def post(self, request, uid: str):
ReceptionContainer(DjangoContainerRepo(), MercadonaReceptionProvider()).execute(Uid(uid))
return Response(status=200)
class ReceptionContainer:
def __init__(self, container_repo: ContainerRepo, reception_provider: ReceptionProvider):
this._container_repo = container_repo
this._reception_provider = reception_provider

def execute(self, container_uid: Uid) -> None:
container = self._container_repo.retrieve_by_uid(uid)
self._reception_provider.provide(container)
class MercadonaReceptionProvider(ReceptionProvider):
def provide(container: Container) -> None:
if container.is_fish():
return MercadonaFishContainerService().call(container.internal_id)

return MercadonaContainerService().call(container.internal_id)

My favorite part about this is that decoupling and separating our code greatly simplifies our testing because now we donā€™t have to test our external services inside our action tests.

We usually have:

  • A happy path integration test in the view (mocking only the external services calls)
  • All the business logic tests inside the action (mocking the injected classes)
  • The database tests inside the repository
  • The external services tests with their logic inside the provider implementation tests

I think thatā€™s all, folks šŸ˜„; actually our use cases are a bit more complex than this, and maybe with this example, we canā€™t see all the benefits of this type of programming, but I promise you, decoupling our code and following SOLID principles and cool tools like Hexagonal Architecture or DDD are really powerful talking about maintainability and readability of our code, and of course are pretty cool šŸ˜¼

Thanks for your time. Any kind of feedback is welcome!

I hope Iā€™ll see you in the next chapter! šŸš€

--

--