Adapter

adapter uml

Opis

Wzorzec projektowy Adapter (Adapter) należy do grupy wzorców strukturalnych. Pozwala na współpracę obiektów o niezgodnych interfejsach. Opakowuje pewną funkcjonalność spełniając konkretny interfejs. Z tego powodu jest też zamiennie nazywany Wrapper.

Mi jednak bliżej do pierwszej nazwy, jako że w języku ojczystym ma dokładnie to samo znaczenie. Dobrze kojarzy się z wszelkiego rodzaju przejściówkami, a to właśnie taką przejściówką jest ten wzorzec projektowy. Określenie jest też dużo bardziej intuicyjne, bo adaptować oznacza dopasowywać. Tak czy inaczej, myślę że trzeba znać obie nazwy, a używać tej, która bardziej pasuje.

Problem i rozwiązanie

Chyba każda aplikacja webowa w dzisiejszych czasach opiera się o integracje. W końcu systemy w Internecie muszą się komunikować. Tak samo jak Wasze aplikacje, inne projekty również nieustannie się rozwijają. Albo co gorsza po prostu padają i nie są już wspierane. W obu przypadkach może pojawić się problem, bo nie zawsze zmiana w zewnętrznych systemach jest możliwa do uwzględnienia w aplikacjach, które z nich korzystają. A trzeba zapewnić ciągłość działania… i to poprawnego działania.

Małe zmiany zazwyczaj można łatwo dostosować. Bywa jednak tak, że konkretne przekształcenie nijak ma się do potrzeb. Wówczas (uwaga najgorsze z możliwych rozwiązań) można dostosować funkcjonalność w kodzie klienckim – i to pewnie w wielu miejscach. To samo jak zewnętrzna biblioteka przestanie być wspierana – cała funkcjonalność do przepisania. A wystarczyłaby przejściówka do której można wpiąć nowe rozwiązanie i najzwyczajniej w świecie działa.

Ostatecznie programista odpowiada za działające rozwiązania w aplikacji, a nie za zewnętrzne biblioteki z których korzysta. Trzeba więc odzyskać kontrolę. A co gdyby zmienić sposób komunikacji. To developer mówi czego potrzebuje i który kod wpuszcza do swojego projektu. Ma pewne gniazdka w postaci interfejsów. Oczywiście zewnętrzny kod nie spełnia tego interfejsu, dlatego przyda się nakładka – tadam: Adapter.

Integracje to nie jedyne miejsce dla Wrappera. Sprawdzi się też przy ograniczeniu albo rozszerzaniu funkcjonalności klasy lub tymczasowo podczas częściowej refaktoryzacji.

Plusy i minusy

Wzorzec projektowy Wrapper z powodzeniem często jest wykorzystywany nieświadomie. W momencie, gdy do serwisu wstrzykiwana jest zewnętrzna zależność, a wszystkie operacje biznesowe wykonywane są w danym serwisie, występuje swego rodzaju adaptacja. Żeby nie zagalopować się za głęboko, lepszym rozwiązaniem jest odseparowanie tego do Adaptera powołując się na zasadę pojedycznej odpowiedzialności (single responsibility).

Co więcej, można wówczas jasno wydzielić funkcjonalność adaptera przez interfejs spełniając zasadę odwrócenia zależności (dependency inversion), a tym samym zyskać elastyczność i wymienność. Mechanizm oparty o ten wzorzec jest też stosunkowo łatwo rozszerzać.

Jedyna wada jaka nasuwa mi się przy jego użyciu to fakt, że może okazać się zbędną warstwą. Pewnie nie chodzi o to, żeby wszędzie i wszystko robić adapterami, chociaż tak naprawdę by się dało. Nie ma jasno określonego miejsca, gdzie go zastosować, ale mimo wszystko wolałbym mieć zbędną warstwę, niż brak elastyczności – tak jak w przypadku fabryki. Na pewno w miejscach na styku warstw, czy nawet całej aplikacji należy go używać.

Przykładowa implementacja w PHP

Zakładam, że ExternalLogger to zewnętrzna biblioteka do zapisywania logów w pliku. W momencie, gdyby przestała być wspierana albo chciałbym zmienić na inną, tym razem obsługującą zapis logów do bazy, byłoby to problematyczne jeśli korzystałbym z niej bezpośrednio. Adapter pozwala mi wpiąć ją w taki sposób, że wymiana klasy nie sprawi problemu. Wystarczy, że nowa klasa spełni interfejs Logger lub tak jak w tym przypadku zrobi to jej adapter.

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Adapter;

use DateTimeImmutable;

class ExternalLogger
{
    private const string FILE_DIR = 'data/log/';

    public function saveLogIntoFile(string $log): void
    {
        $logFile = self::FILE_DIR . 'log_' . date('d-M-Y') . '.log';

        file_put_contents($logFile, $this->createLogMessage($log), FILE_APPEND);
    }

    private function createLogMessage(string $message): string
    {
        return sprintf("%s: '%s'\n", (new DateTimeImmutable())->format('H:i'), $message);
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Adapter;

interface LoggerInterface
{
    public function log(string $logMessage): void;
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Adapter;

final class LogAdapter implements LoggerInterface
{
    public function __construct(
        private ExternalLogger $logger
    ) {}

    public function log(string $logMessage): void
    {
        $this->logger->saveLogIntoFile($logMessage);
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Adapter\Test;

use DesignPatterns\Structural\Adapter\ExternalLogger;
use DesignPatterns\Structural\Adapter\LogAdapter;
use DesignPatterns\Structural\Adapter\LoggerInterface;
use PHPUnit\Framework\TestCase;

final class LogAdapterTest extends TestCase
{
    public function testLogAdapterImplementsLogInterface(): void
    {
        $logAdapter = new LogAdapter(new ExternalLogger());

        self::assertInstanceOf(LoggerInterface::class, $logAdapter);
    }

    public function testCanLogIntoFile(): void
    {
        $message = 'Test log message';
        $externalLogger = $this->createMock(ExternalLogger::class);
        $logAdapter = new LogAdapter($externalLogger);

        $externalLogger
            ->expects($this->once())
            ->method('saveLogIntoFile')
            ->with($message);

        $logAdapter->log($message);
    }
}

Adapter – podsumowanie

Ten wzorzec projektowy jest łatwy w implementacji, a daje wymierne korzyści. Zapewnia ustrukturyzowaną komunikację między dwiema klasami. Pasuje w wielu miejscach, co jest dużą zaletą, ale też problemem jako że może okazać się over-engineeringiem. Korzystajcie z niego swobodnie, a tak jak wspominałem – lepiej go mieć, niż gdyby miało go zabraknąć. Szczególnie, że jest to wzorzec, który w przeciwieństwie do wielu innych nie psuje czytelności i nie komplikuje kodu (reguła KISS).

Świetna rzecz! Pozwolić zewnętrznym bibliotekom zaadoptować się do własnych reguł i tym samym mieć pełną kontrolę nad integracjami i wykorzystaniem zewnętrznego kodu w aplikacji. Dobrze zrobiony mechanizm tego typu zapewni wymienialność rozwiązań.

Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan dobrego humoru i podcastów.