Do you write your application for better future sustainability or just to get paid for it today? If you're the first one, you care about design patterns. I'm happy to see you!
Today I will show you why and how to use delegator pattern in your application so it makes it to the pension.
"Every code is trash!"
You'll see. But before we dig into code... what are the reasons to write sustainable code and how it looks like?
There are 3 levels of developers by the time-frame focus they work on. Every group has it's advantages and disadvantages. You'll soon see which one you fit in.
This project. Single site for 2018 elections. Microsite for new product release in 2019. Include anything that is hype in socials last year.
If the code would be a trash (literally!), they'd throw everything to 1 bag or maybe right in the city streets or nature. Someone else will handle cleaning up the city #yolo
The project has tests, continuous integration, uses stable packages with 1.0 release. It's startup or a project with profit. The team is fine and slowly growing. It's their first or second project and they try to take good care about it, with experiences they have.
They don't make any mess around the city and put all trash to 1 trash bin. Take them out regularly once a week. They're nice to the world. Well, at least at first sight.
...or at least with that mindset in their minds. The code won't probably work with PHP 9.0, but they do their best to make it as easy as possible to do so.
They have great experience with handful of project described in previous group. They already worked on 5 open-source projects they need to last as long as possible without as little maintenance as possible.
To the trash again...
It's like recycling plastic bags, glass bottler and papers.
You put effort to it:
Though you never see the trash again, you believe it's good for your future self and for your children, to keep planet clean and away from trash lands. Economists would call it positive externality.
Now you know why it's good to separate waste (= code), let's get to real code.
Let's have a project that was born in 2015 and see how it slowly grew. It will eventually use all patterns we described in the beginning - except delegator, which is unfortunate for the investors of this project.
Project start with few controllers that contain most of logic. It's fast and easy to add new controller with new logic.
By the end of the year there are 50 controllers like this:
class ProductController extends Controller
{
public function allAction()
{
$allProducts = $this->getEntityManager()->getRepository(Product::class)
->fetchAll();
return new TemplateResponse('all.twig', [
'allProducts' => $allProducts
]);
}
}
Also, it's in the documentation of the framework, so it must be the best practise.
Little we know, here starts our Broken Window Theory, the most underestimated effect from social science in software world.
Application grows and the size needs pre-caching handled by running commands in CRON. So you start using Symfony\Console. You get inspired by Controller
, because Command
looks like it and by the end of year, there are many command like this one:
class CacheProductsCommand extends Command
{
/**
* @var EntityManager
*/
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function execute()
{
$allProducts = $this->entityManager->getRepository(Product::class)
->fetchAll();
// cache them all
}
}
It's 2017, AI is on hype and you start thinking about product recommendation feature. You use EventSubscribers that saves many information about user behavior and return best producs just for him.
class RecommendedProductsEventSubscriber implements EventSubscriber
{
/**
* @var EntityManager
*/
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public static function subscribe()
{
return ['onPageVisit' => 'setRecommendedProducts'];
}
public function setRecommendedProducts(BehaviorPatternEvent $behaviorPatternEvent)
{
$productRepository = $this->entityManager->getRepository(Product::class);
$product = $productRepository->findBestByBehavior($behaviorPatternEvent->getBehavior());
$behaviorPatternEvent->setRecommendedProducts($product;
}
}
So far so good?
"Change is the only constant."
New owner with technical skills comes the the play. And he wants to finally use VueJs
, the company is now big enough to use Docker as standards and there are more programmers that know Eloquent than Doctrine in his country:
"Alibaba is catching up and we might lose the position #1 leader on market. Just switch it to Eloquent, so we can hire and on board faster."
Ups! Your code is coupled to the Doctrine and Symfony pretty hard. You're standing in front of important question: Do you get extra $ 10 000 to refactor the code?
Posing this question, now we finally understand Broken Window Theory...
...because we have personal experience with going it the wrong way. Little to late.
No. You think for the future with prevention!
"Plan like you will live forever, and then live like there is no tomorrow."
Same can be applied to your code.
This is what we did in Lekarna.cz - The biggest online drugstore in the Czech Republic. It started on Nette 2.4 and Doctrine 2.5, with monorepo approach.
When a class pattern is marked as delegator, it can't contain any direct connection to database layer (Doctrine in this case).
Among most popular delegators belongs:
In Lekarna, these classes can only use own service to access products - ProductRepository
:
class ProductRepository
{
public function __construct(EntityManager $entityManager)
{
$this->repository = $entityManager->getRepository(Product::class);
}
public function fetchAll()
{
return $this->repository->fetchAll();
}
}
You don't want to check this in code reviews (imagine 5 years doing it), just write a sniff for that and forget it.
This will remove any database layer reference from all our delegators
:
class CacheProductsCommand extends Command
{
/**
* @var ProductRepository
*/
private $productRepository;
public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}
public function execute()
{
$allProducts = $productRepository->fetchAll();
// cache them all
}
}
Do you need to switch database layer? Easy!
class ProductRepository
{
- public function __construct(EntityManager $entityManager)
- {
- $this->repository = $entityManager->getRepository(Product::class);
- }
+ public function __construct(Eloquent $eloquent)
+ {
+ $this->repository = $eloquent->getRepository(Product::class);
+ }
public function fetchAll()
{
return $this->repository->fetchAll();
}
}
1 day of work instead of hundreds of hours. That's what delegator pattern is all about.
When you start with the best known approach possible, you'll end-up in well grown project that you'll love to contribute more the older it gets.
Just like with children - invest in them right from the start and it will get back to you!
Happy Children and Project Raising!
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!