How decorators can help you to add features without breaking your code

Docler
Byborg Engineering
Published in
4 min readMay 4, 2020

Written by João Marcos Bizarro Lopes

How can a software developer change a feature adding a pre-process or post-process operation without changing anything on the existing code and without breaking any existing unit tests? Beyond that, how can a developer do that all without over injecting dependencies into existing structures?

Before we start

All code samples you will see during this article are written in PHP 7.1 and the framework is Laravel 5.5.

Context

Joe is a software developer at UniFake. Last sprint he had developed a student registration process, well written and tested. On the current sprint, the IT manager wants to measure how long the registration process takes and the security manager wants to add an IP blacklist to the process to avoid attacks.

Joe’s architecture

This is how the code is structured now

Joe’s architecture

Joe doesn’t want to change his controller since it’s acting just as a delegate to its respective service.

And the service abstraction looks like:

Observation: this request class doesn’t need to be abstract to be part of the abstraction, but I’ve added it here for didactic purposes.

Finally, this is the adapter controller is using to forward the request to the service

The service implementation and its unit tests looks like this:

I’ve decided to use stubs rather than mocks to make interface segregation clearer.

First approach

Joe’s first idea is to create static classes with static methods and place them both in register student service.

Let’s analyze this approach. What is wrong in this code?

  • Violates Single Responsibility Principle: this class is now responsible not only for executing the registration process but also for security and performance;
  • Violates Open Closed Principle: When Joe added those static classes and methods he modified the object behavior instead of extending it.
  • Partially violates Liskov Substitution Principle: Joe cannot substitute this implementation by another one without copying security and performance code to it, but why partially? Partially because it’s not a technical error, but semantic. In this case, the new class would not have the obligation to have performance and IP blacklist logic, but the stakeholders would be there expecting to see them.
  • Violates Dependency Inversion Principle: Joe’s service class is now tightly coupled to performance calculator and IP blacklist implementations.

Besides that, using static classes and methods usually produces a code smell called Ambient Context. This is the definition of Ambient Context by Mark Seemann:

An Ambient Context supplies application code outside the Composition Root with global access to a Volatile Dependency or its behavior by the use of static class members.

Finally, imagine how this service class’s tests will behavior after these changes. Joe tests would need to care about security and performance even if those tests should be testing student registration only.

Joe realizes the mistakes he have made and move towards a second better approach.

Second approach

Joe’s second approach is to create interfaces for both performance and security logic and inject them into service’s constructor.

Ok, this approach sounds better.

It fixes the Dependency Inversion violation and removes completely the Ambient Context code smell. It also makes tests easier again although the they still care about security and performance.

However, this code doesn’t solve the violation of SRP, OCP and LSP principles and beyond that, it produces another code smell called Constructor Over Injection.

Constructor Over-Injection occurs when there are too many required Dependencies specified as parameters in the class’s constructor.

This code smell usually makes testing hard and extending even harder, but still is better than Joe’s first approach.

Third approach

Joe knows that he needs to move his security and performance logic to another object to solve his SOLID principles violations. How to do it then?

  • He could inject these dependencies into the controller and call the methods from there: smart idea, but Joe would just be moving his problem from service class to controller class;
  • He could create a Laravel middleware and solve all his problems for good: excellent idea, but Joe needs to consider the trade-offs of his choice. It’s an easier option, but he’d couple his middleware to the framework. It’s not a bad option after all;
  • He could create decorators and compose his service class with them: in this case, Joe fixes all problems with a sophisticated code, but at the same time it takes more time to develop it comparing to Laravel middlewares.

In order to create decorators for student registration service class, the decorators need to implement student registration interface and compose themselves with another instance of student registration interface.

Finally, it’s just about composing the object tree in the Composition Root:

It seems a good solution since it fixes the SOLID violations and new classes have no Ambient Context anti-pattern or Constructor Over Injection code smell, but Joe still has issues, right?

What is wrong?

Imagine Joe needs to apply the same decorators in other services (student enrollment, for example), how would he do it? His PerformanceCalculator and IPBlacklist classes are implementing a specific method from the student registration interface, which is expecting RegisterStudentRequest as the parameter. In other words, Joe’s decorators are coupled with student registration service.

How to fix it?

Joe can make his decorators generic if his services have a generic abstraction, like:

Unfortunately by fixing this problem, Joe creates another one. How would he bind this interface to many different implementations?

Tricky, right?

Actually, Joe would be able to solve this easily if he had written his software in a language that supports generics such as C#, but before you start throwing tomatoes at me, let me tell you that there is a great solution in PHP.

Next article I will demonstrate how to create, in PHP, the same generics behavior as in C#, allowing you to improve the usage of decorators showed in this article.

If you have any doubts or suggestions leave a comment or feel free to send me an email (jmbizarrolopes@gmail.com).

--

--

Docler
Byborg Engineering

Curious about the technologies powering the 30th most visited website in the world, with 45 million users, 2,000 servers, 4 data centers?