JesusValera

Why and How to decouple your classes

with a real decoupling example

 Found a typo? Edit me

caravaca-fuentes-marques-1

There are some ways to couple your code, some frameworks like Laravel embrace you to do using their own façades or even using ORM as ActiveRecord, but this is also possible using PHP native methods like time() beyond others.

Unfortunately, in the long run, if you don't invert the dependencies, the code will get messy faster/easier, hurting your application. To avoid this, you must write tests. Writing tests help you realize how coupled is your business logic to infrastructure code.

Examples of classes that should be decoupled

Belongs to the Programming language itself like

Belongs to the infrastructure code like

Decoupling a real example

For the next example, let’s try to use a Laravel facade and a PHP function.

Imagine we have the following service, and we really want to invert the dependencies. As you can see, we have the trans() facade and the date() PHP function.

namespace App\Application;

final class WelcomeService
{
    /**
     * Output: "hello {$name} @ {Y-m-d}"
     */
    public function welcome(string $name): string
    {
        return trans('messages.welcome', [
            'name' => $name,
            'date' => date('Y-m-d'),
        ]);
    }
}

The first thing we should do is to move the trans() Laravel function logic to a different service, in order to do that, we have to invert the dependencies introducing a new interface as follows:

namespace App\Domain;

interface Translator
{
   public function trans(string $message, string ...$placeholders): string;
}

And we can place in this class the original Laravel implementation.

namespace App\Infrastructure;

final class LaravelTranslator implements Translator
{
   public function trans(string $message, string ...$placeholders)
   {
      return trans($message, ...$placeholders);
   }
}

From now on, we are able to switch the translator implementation between Laravel or a third-party one by implementing the Translator interface.

We can inject the new LaravelTranslator service in our WelcomeService defining in the service container which implementation we want to resolve when the framework faces the Translator in the constructor.

namespace App\Infrastructure;

final class WelcomeService
{
    public function __construct(
        private Translator $translator,
    ) {}

    public function welcome(string $name): void
    {
        return $this->translator->trans(
            'messages.welcome',
            ['name' => $name, 'date' => date('Y-m-d')],
       );
    }
}

The next step is to do the same but with the date() method.

namespace App\Domain;

interface Date
{
   function toString(): string;
}

And the PHP implementation:

namespace App\Infrastructure;

final class SystemDate implements Date
{
    public function toString(): string
    {
        return date('Y-m-d');
    }
}

Let’s do the same as we did previously, but this time we can inject our own SystemDate implementation through the Date interface.

namespace App\Application;

final class WelcomeService
{
    public function __construct(
        private Translator $translator,
        private Date $date,
    ) {}

    public function welcome(string $name): void
    {
        return $this->translator->trans(
            'messages.welcome',
            ['name' => $name, 'date' => $this->date->toString()],
       );
    }
}

Finally, we can create a test for the WelcomeService injecting the required dependencies.

final class WelcomeServiceTest extends TestCase
{
    public function test_welcome(): void
    {
        $translate = new LaravelTranslator(); // Real implmentation

        $date = $this->createStub(Date::class); // Fake implementation
        $date->method('toString')->willReturn('2022-02-22');

        $name = 'World';

        $service = new WelcomeService($translate, $date);
        $response = $service->welcome($name);

        self::assertSame('hello World @ 2022-02-22', $response);
    }
}

Conclusion

Decoupling your classes is a good practice that will help you to maintain your codebase in the long run.
It will also help you not only to switch between different implementations of the same interface in case you decided to change the framework or the third-party service but also to test your code in a more efficient way.

caravaca-fuentes-marques-2