Quantcast
Channel: Nicolò Pignatelli
Viewing all articles
Browse latest Browse all 10

Dependency inversion

$
0
0

Here at Rocket Labs, we run weekly workshops about clean code and good development practices. During workshops, we introduce some topic and we try to immediately apply it to our codebase. This time was my turn to lead and I chose to discuss about dependency inversion.

The definition

It's the D in the SOLID acronym.
It states:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.1

Plugs and sockets

(Hopefully) every apartment has electric sockets. We use sockets to connect and supply power to electrical devices. In our case, the socket is the abstraction, while the devices are the details; every device has been designed following the constraints imposed by the socket (i.e. every device has a plug compatible with the socket). We don't have a different socket for each plug, nor the socket makes distinctions on which plug is being connected. The details depend upon the abstraction, plugs and sockets are an example of good design.

A common starting point

In our daily work, it's not unusual to see code like this2:

<?php // EmailTokenService.php

class EmailTokenService
{
    public function isEmailTokenValid($emailToken)
    {
        $userModel = new UserModel();
        $userRow = $userModel->getByEmailToken($emailToken);

        if (empty($userRow)) {
            return false;
        }

        $emailTokenCreatedAt =
            new DateTimeImmutable($userRow['email_token_created_at']);
        $yesterday = new DateTimeImmutable('-1 day');

        return $emailTokenCreatedAt > $yesterday;
    }
}

This code is quite trivial and easy to understand, but it doesn't respect the dependency inversion principle, since the service directly depends on the UserModel implementation to retrieve information about the email token and execute the related business logic. How can we fix that?

The greenfield solution

A quick search on Google will show you how to achieve DI in your shiny new class:

your class should depend upon interfaces; therefore, define them first and make your "real" dependencies simply implement those interfaces.

Quite easy, but in our situation this is not enough. Let's see why.

Let's start by changing the definition of our EmailTokenService to depend only upon a new interface named EmailTokenInformationProvider. Let's also use dependency injection3 to avoid instantiating the dependency directly in our service class:

<?php // EmailTokenService.php

final class EmailTokenService
{
    private $emailTokenInformationProvider;

    public function __construct(EmailTokenInformationProvider $emailTokenInformationProvider)
    {
        $this->emailTokenInformationProvider = $emailTokenInformationProvider;
    }

    public function isEmailTokenValid($emailToken)
    {
        $emailTokenCreationTime = $this->emailTokenInformationProvider->getCreationTime($emailToken);

        $yesterday = new DateTimeImmutable('-1 day');

        return $emailTokenCreationTime > $yesterday;
    }
}

<?php // EmailTokenInformationProvider.php

interface EmailTokenInformationProvider
{
    /**
     * @param string $emailToken
     * @return DateTimeImmutable
     */
    public function getCreationTime($emailToken);
}

Now my UserModel class should implement the EmailTokenInformationProvider, providing the implementation of getCreationTime($emailToken). But this leads to several problems:

  1. Being built with another purpose in mind, UserModel would implement an interface unrelated to its context.
  2. We would need to change the definition of the UserModel class to fulfill the interface, and maybe this is not even possible.

Let's see how can we solve this.

The Adapter pattern

In software engineering, the adapter pattern is a software design pattern that allows the interface of an existing class to be used from another interface. It is often used to make existing classes work with others without modifying their source code.4

We will consider our UserModel as a plug that needs an adapter to be able to be connected to the EmailTokenService socket. We are going to declare another class that will act as an Adapter for the UserModel. This class, named UserEmailTokenInformationProvider, will use composition to achieve the Adapter pattern and will implement the EmailTokenInformationProvider interface:

<?php // UserEmailTokenInformationProvider.php

final class UserEmailTokenInformationProvider implements EmailTokenInformationProvider
{
    private $userModel;

    public function __construct(UserModel $userModel)
    {
        $this->userModel = $userModel;
    }

    public function getCreationTime($emailToken)
    {
        $userRow = $this->userModel->getByEmailToken($emailToken);

        if (empty($userRow)) {
            throw new EmailTokenNotFound($emailToken);
        }

        $emailTokenCreatedAt =
            new DateTimeImmutable($userRow['email_token_created_at']);

        return $emailTokenCreatedAt;
    }
}

We are now able to correctly instantiate our EmailTokenService:

<?php // app.php

$userModel = new UserModel();
$userEmailTokenInformationProvider = new UserEmailTokenInformationProvider($userModel);
$emailTokenService = new EmailTokenService($userEmailTokenInformationProvider);

$isEmailTokenValid = $emailTokenService->isEmailTokenValid($emailToken);

Results

Our email token system is now decoupled from the User model; furthermore, introducing an intention revealing interface, we added clarity and explicitness to our code.

A nice side effect of our refactoring is the ability to unit test the EmailTokenService business logic in a very simple way:

<?php // EmailTokenServiceTest.php

/** @test */
public function email_token_validity(DateTimeImmutable $creationTime, $expectedValidity)
{
    $emailTokenInformationProvider = $this->getMock(EmailTokenInformationProvider::class);
    $emailTokenInformationProvider->method('getCreationTime')->willReturn($creationTime);

    $emailTokenService = new EmailTokenService($emailTokenInformationProvider);
    $isEmailTokenValid = $emailTokenService->isEmailTokenValid('token');

    $this->assertSame($expectedValidity, $isEmailTokenValid);
}

The only tradeoff of our refactoring is the increased amount of classes and code that we have to maintain. Personally, I prefer to code and deal with a high amount of small classes rather than with fewer, more cluttered ones.


1. Martin, Robert C. (2003). Agile Software Development, Principles, Patterns, and Practices. Prentice Hall. p. 127. ISBN 978-0135974445.

2. I use PHP in my sample code, but please note that DI is a concept that transcends the particular language of choice.

3. Note for the reader: Dependency injection is not the same as Dependency inversion.

4. See http://en.wikipedia.org/wiki/Adapter_pattern.


Viewing all articles
Browse latest Browse all 10

Trending Articles