konie

Dependency Injection na prostym przykładzie w PHP

Cześć! Dzisiaj na tapecie wzór projektowy wstrzykiwanie zależności. Co znajdziesz w tym wpisie:

Czym jest wstrzykiwanie zależności?

Jest to przekazywanie potrzebnej zależności poprzez argument zamiast tworzenia jej. Zanim poznałem DI, miałem zwyczaj pisać kod który w locie tworzył sobie potrzebne zależności. Przykład z życia wzięty:

<?php declare(strict_types=1);

namespace app;

use Aws\Result;
use Aws\S3\S3Client;

class S3
{
    /**
     * @var \Aws\S3\S3Client
     */
    private $s3Client;

    public function __construct()
    {
        $this->s3Client = new S3Client([]);
    }

    public function putObject(array $args = []) : Result
    {
        return $this->s3Client->putObject($args);
    }
}

Jak byś przetestował tą klasę? Ja widzę tylko jedną opcję:

  1. Tworzysz nową instancję,
  2. Za jej pomocą wgrywasz plik na S3,
  3. Weryfikujesz czy plik został wgrany na S3.

Skomplikowane prawda? Wynika to z faktu że klasa jest silnie (wręcz nierozerwalnie) powiązana z klasą klienta S3. Nie możemy przekazać imitacji klienta. To powoduje następujące konsekwencje:

  • nie da się napisać testu jednostkowego – test jest integracyjny,
  • testowanie wymaga połączenia sieciowego oraz poprawnej konfiguracji S3,
  • takie test będzie trwał dużo dłużej niż prosty test jednostkowy.

Jak rozwiązać ten problem? Użyj wstrzykiwania zależności!

Jak je zaimplementować?

Tej klasie do działania potrzebna jest instancja klienta S3. Przekażę ją przez argument konstruktora:

<?php declare(strict_types=1);

namespace app;

use Aws\Result;
use Aws\S3\S3Client;

class S3
{
    /**
     * @var \Aws\S3\S3Client
     */
    private $s3Client;

    public function __construct(S3Client $s3Client)
    {
        $this->s3Client = $s3Client;
    }

    public function putObject(array $args = []) : Result
    {
        return $this->s3Client->putObject($args);
    }
}

Alternatywną opcją jest użycie settera. Dzięki temu podejściu możemy w łatwy sposób przekazać właściwą instancję klienta S3. Prawdziwą w środowisku produkcyjny a w środowisku testowym imitację.

Implementacja testu w PHPSpec:

<?php declare(strict_types=1);

namespace spec\app;

use Aws\Result;
use Aws\S3\S3Client;
use PhpSpec\ObjectBehavior;

class S3Spec extends ObjectBehavior
{
    function it_puts_object_using_aws_s3_client(S3Client $s3Client, Result $result)
    {
        $args = ['test'];
        $s3Client->putObject($args)->shouldBeCalledOnce()->willReturn($result);
        $this->beConstructedWith($s3Client);
        $this->putObject($args)->shouldBe($result);
    }
}

Cały test jest bardzo prosty do napisania, trwa 93ms 🙂

Jakie ma plusy i minusy?

Zacznę od plusów:

  • ułatwia a czasem wręcz umożliwia pisanie testów jednostkowych,
  • rozluźnia powiązanie między klasami, w przypadku użycia interfejsów czy klas abstrakcyjnych powiązanie jest jeszcze słabsze,
  • wyprowadzania logikę tworzącą obiekty w inne miejsca

Minusy:

  • gdzieś te obiekty trzeba stworzyć, w prostych projektach tworzę klasy fabryki, w większych zdecydowanie polecam użycie kontenera wstrzykiwania zależności,
  • kod może się wydawać trudniejszy do zrozumienia.

Linki

3 thoughts on “Dependency Injection na prostym przykładzie w PHP”

  1. By using Dependency Injection we can write more maintainable, testable, and modular code. All projects have dependencies. The larger the project the more dependencies is it bound to have; now having a great number of dependencies is nothing bad by itself however how those dependencies are managed and maintained is.

Comments are closed.