loading...
Cover image for Protect your system from changes in 3rd party dependencies

Protect your system from changes in 3rd party dependencies

aleksikauppila profile image Aleksi Kauppila ・3 min read

There's always risks when using third party code. An open source library that i chose for my project may not be maintained anymore. Or, upgrading another dependency may have backward incompatible changes. The library may have an awful interface but the service it provides outweighs the negatives.

Everyday work is usually tasked with solving business related problems. Having to develop and maintain general purpose tools or libraries is usually not something that the customer is willing to pay. So we use open source libraries.

We also want to be smart and protect our application against changes in third party dependencies. We wan't to keep our dependencies up-to-date, like we keep our operating systems.

First, let's look at a bad example of how to deal with third party dependencies. I'll use the popular HTTP client guzzlehttp/guzzle as an example here. We want to use a HTTP client to make requests to an external system.

use GuzzleHttp\Client;

class ExternalService
{
    private $httpClient;

    public function __construct()
    {
        $this->httpClient = new Client();
    }
    // ...
}

In this example we are liberally declaring a dependency inside the constructor. This will prevent us from unit testing this class. We should be using dependency injection here.

The other problem in this example is that now we've tied ourselves to a concrete dependency. We should be considering dependency inversion principle which states that we should not depend on implementation details of lower level classes.

How do we make this right?

Design the interface you want to use

We'll define a HTTP client interface.

use Psr\Http\Message\ResponseInterface;

interface HttpClient
{
    public function get(string $uri, array $options): ResponseInterface;
    public function post(string $uri, array $options): ResponseInterface;
    public function put(string $uri, array $options): ResponseInterface;
    public function patch(string $uri, array $options): ResponseInterface;
    public function delete(string $uri, array $options): ResponseInterface;
}

Write an adapter for Guzzle's HTTP client

We'll make use of Guzzle's capabilities but hide it behind our own interface.

use GuzzleHttp\ClientInterface;
use MyApp\HttpException;

final class GuzzleHttpClientAdapter implements HttpClient
{
    private $client;

    public function __construct(ClientInterface $client)
    {
        $this->client = $client;
    }

    public function get(string $uri, array $options): ResponseInterface
    {
        return $this->request("GET", $uri, $options);
    }

    public function post(string $uri, array $options): ResponseInterface
    {
        return $this->request("POST", $uri, $options);
    }

    // ... other required methods

    private function request(string $method, string $uri, array $options): ResponseInterface
    {
        try {
            return $this->client->request($method, $uri, $options);
        catch (\Exception $exception) {
            // This is an exception we defined in our application
            throw new HttpException($exception->getMessage(), $exception->getCode(), $exception);
        }
    }
}

This way we've effectively restricted the usage of a third party library to a single method call in the whole application. Changing or upgrading the library will not cause ripple effect through our whole application.

* Adapter pattern

Use the new interface in client code

Finally, when we need an HTTP client in our application we must declare the dependency using the interface. If we designed the interface well, changing the dependency will require us only to write a new adapter and inject it where it's needed. The users of HttpClient will stay completely unaware of - and unaffected to - these changes.

use MyApp\HttpClient;

class ExternalService
{
    private $httpClient;

    public function __construct(HttpClient $httpClient)
    {
        $this->httpClient = $httpClient;
    }
}

// ...

$extService = new ExternalService(new GuzzleHttpClientAdapter(new Client()));

Posted on by:

Discussion

markdown guide