Or: How to kill off the if branch.
Everli has changed a lot in the past years! Not only by growth in raw numbers, but structurally.
- From just Italy into multiple countries.
- New kinds of business partnerships and customer relationships.
- New types of retailers.
(And that’s not even mentioning external changes like new government regulations)
Feature Creep Can’t Be Stopped
So imagine you’ve got some code. It’s readable, it met all the requirements, very elegant!
”Okay code, now meet the real world.”

No matter how good the code was before, requirements change.
- A new country is added with its own basket of regulations.
- Or a country brought in a new law – something is different now just for them.
- Or a retail partner changes their agreement – they need something different just for them.
So you added a small fix, one “little” if
branch to handle that special case?
- Requirements keep changing – until the project is dead.
- Requirements become more complex over time – usually added, not removed.
You soon end up with a sprinkling, then a heap, then a mountain of if-else
branches through your code!
Yes, that is a bad thing.
- You easily lose sight of the historical reason for each “little” branch, especially across multiple files.
- Testing every combination of them is a nightmare.
- Even just reading through them can be a challenge.
(not naming and shaming any 400+ line methods here, no…)

So, when you write that code?
Expect requirements and business logic to change in the future, in ways you can’t anticipate.
Make change less painful for future developers. (Let’s be honest, probably it will be you)
Putting Decoupling and Cohesion into Practice
These should be familiar terms to any student of program design. In short;
- Decoupling: Keep different things away from (and independent of) each other,
- Cohesion: Keep related things together.
Code is easier to change if change happens in just one place.
For scalable business logic; keep it away from the program flow, keep it away from each other.

How can this be done?
1) Generic Program Flow
The program flow (the sequence of steps – doThis()
, then doThat()
, then doTheOtherThing()
) is separated from any of the business-logic-tainted specifics of how those steps are to be performed.
- It doesn’t even know which implementation should be used.
2) Business Logic
Each specific implementation of business logic (how to doThis()
for a particular situation, eg a particular retailer + country + phase_of_moon
) lives in its own space.
- It’s unaware of the wider program flow.
- It’s unaware of any other implementations.
3) Self-Selection
Each specific implementation of business logic is self-selecting – it decides whether it should be used for a particular situation.
- Selection and implementation are kept together.
4) Clear Separation
The point where core program flow and specific implementations interact has a clear interface.
- From a program-flow point of view, it’s clear what the implementation should do.
5) Isolated Changes
A change to the program flow can be made in isolation of any specific implementation.
A change to a specific implementation can be made in isolation of the core logic.
- Changes happen in just one place.
Show Me Some Code!

and the core program flow becomes:

So that’s basically it.
Calling back to the points above:
Generic Program Flow
The core-logic code callsresolve()
. It doesn’t know anything about which implementation it’s using.Business Logic
Each implementation is isolated into its own class, and handles just that one set of business logic.Self-Selection
Each implementation chooses whether to execute some task.Clear Separation
Interaction between core-logic and implementations is defined by the interface.
That interface is free to define additional methods, parameters, return types beyond just theaccepts()
method.Isolated Changes
Changes are almost completely independent.
A change to the Blue implementation? Modify only theBlue
class.
A new Purple implementation is added? Create thePurple
class, addPurple
to the list.

Final Implementation (Laravel)
Many of our projects use the Laravel framework, so let’s talk briefly about our implementation of this pattern.
The Service Container is the right place to keep the list of implementations, using the tagging feature.
- Laravel has a mechanism for this – use it.
- The core program flow doesn’t know anything about the implementations.
- During unit tests the implementations can be completely swapped out for a test mock.
This gives the ability to test core program flow in complete isolation.
1) Tag the implementations with their interface:
<?php declare(strict_types=1);
namespace App\Providers;
...
final class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
...
// Specific color implementations - use first that accepts
$this->app->tag([
DarkGreen::class,
Green::class,
Red::class,
Blue::class,
], ColorInterface::class);
}
}
2) Find the right implementation for that context:
/**
* Fetch the correct Color implementation for this context.
*
* @param Context $context
*
* @return ColorInterface
*/
protected function resolveColor(Context $context): ColorInterface
{
/** @var ColorInterface $color */
foreach (app()->tagged(ColorInterface::class) as $color) {
if ($color->accepts($partner)) {
return $color;
}
}
throw new LogicException("No color found for context {$context->toString()}");
}
3) Call the right implementation, without needing to know which:
/**
* Some important program flow.
*/
public function doTheThing()
{
...
$color = $this->resolveColor($context);
$color->execute($context);
// and call other methods, as needed
...
}
easy.
Extra: Unit Testing Helper
One limitation of the Laravel container is that it doesn’t allow existing tags to be replaced, only appended.
(Making it difficult to inject a test mock into the container)
For unit testing, a little reflection can overcome this and allow replacement.
Include this trait in unit test classes that need that ability.
ModifiesContainer.php
<?php declare(strict_types=1);
namespace Tests;
use ReflectionClass;
/**
* Helpers that alter the behaviour of the application container.
*
* @see https://laravel.com/docs/6.x/container
*/
trait ModifiesContainer
{
/**
* Replace an existing tag with the given mapping.
*
* (Provides a missing feature - the container doesn't allow tags to be removed, only added)
*
* @see https://laravel.com/docs/6.x/container#tagging
*
* @param array<object|string> $classes List of class names or instances
* @param string $tag Tag to replace
*
* @return void
*/
private function replaceTag(array $classes, string $tag): void
{
foreach ($classes as $i => $class) {
if (is_object($class)) {
$this->app->instance(get_class($class), $class);
$classes[$i] = get_class($class);
}
}
$appReflection = new ReflectionClass($this->app);
$tagsReflection = $appReflection->getProperty('tags');
$tagsReflection->setAccessible(true);
$tags = $tagsReflection->getValue($this->app);
$tags[$tag] = $classes;
$tagsReflection->setValue($tags);
}
}
So that’s it!

Some Finer Points
- The order of the classes can be important.
As shown above, theDarkGreen
class must be placed before theGreen
class.
(Because it is a specialization of theGreen
case) - One implementation can still inherit from another.
DarkGreen
does mostly the same thing asGreen
, so delegates back to the parent for shared behavior.
(This will add a dependency between those two classes, so the usual warnings apply) - This pattern can be used even when there’s only a single implementation available!
In some cases we use this pattern, confident that it will be needed in the near future.
In a different circumstance, the program might want to run all accepting implementations.
This begins to look like a publish-subscribe pattern, or could be built with a method likeresolveAll
returning a collection of accepting implementations.
We strive to create software that is not only efficient for our stakeholders and customers, but that solves problems they don’t even know they will have.
If you think you are brave enough, and you have what it takes to solve it, visit our careers site to see our current openings.
Author: @MrTrick