Wzorzec adapter

Strukturalny wzorzec projektowy adapter to spore udogodnienie w walce z systemami legacy. Pozwala użyć niepasującego interfejsu w innym. Dzięki temu zabiegowi pomimo, iż nie posiadamy spójnej abstrakcji możemy zastosować pożądaną logikę.

Co to jest wzorzec adapter?

Strukturalny wzorzec projektowy, który idealnie nada się, jeżeli znaleźliśmy się w sytuacji, kiedy musimy użyć dwóch niekompatybilnych ze sobą interfejsów. Inaczej mówiąc, poprzez użycie adaptera możemy skonwertować jeden interfejs do postaci, którą akceptuje inny. Można wyobrażać sobie ten wzorzec, jako pewnego rodzaju opakowanie, które jest gotowym do użycia rozwiązaniem, spełniającym wymaganie implementacyjne, które wcześniej było niepoprawne w docelowym miejscu wywołania. Jednocześnie, nikt nie wie o konwersji, która zaszła w tle.

Teraz bardziej życiowo… Jeżeli chcesz użyć karty SIM w standardzie NANO, w telefonie, który akceptuje karty Micro SIM, to musisz zastosować adapter, który zwiększy rozmiar Twojego SIMa. W efekcie czego, użyjesz telefonu ze swoją kartą, a sam telefon nawet nie zda sobie sprawy co się stało.

Przykład wzorca adapter

Wyobraźmy sobie, że w naszym systemie do pewnego momentu zapisywaliśmy logi w tabeli, w bazie danych. Po pewnym czasie otrzymujemy zadanie przerobienia sposobu działania tak, aby logi zapisywane były u zewnętrznego dostawcy. Myślisz bułka z masłem, a następnie widzisz poniższą spuściznę poprzedniego zespołu deweloperskiego.

class Logger {
  constructor(
    private readonly logRepository: LogRepository
  ) {}

  async logInDb(dto: LogDto) {
    const log = Log.create(dto);

    await this.logRepository.save(log);
  }
}

class SomeService {
  constructor(
   // ...
   private readonly logger: Logger
  ) {}

  async a() {
    // ...

    await this.logger.logInDb(/** ... */);
  }

  async b() {
    // ...

    await this.logger.logInDb(/** ... */);
  }

  // ...
}

Zewnętrzny dostawca udostępnia nam dedykowane SDK, z prostą funkcją log.

class DataCatLogger {
 log(data: LogData) {
    // POST req using http lib
  }
}

Co teraz zrobisz, jeżeli system jest duży, miejsc do zmiany jest około 1000, a deadline na wczoraj? Wszędzie zmienisz zależność, nazwę metody i poprawisz testy?

Potrzebna przejściówka

Łatwiejszym sposobem będzie zastosowanie wzorca adapter i zmiana wstrzykiwanego obiektu „w locie”, tam gdzie będzie to potrzebne. Stwórzmy więc klasę DataCatLoggerAdapter, która rozszerzy bazowy Logger zmieniając logikę metody logującej tak, aby używała SDK zewnętrznego dostawcy.

class DataCatLoggerAdapter extends Logger {
  constructor(
    private readonly logRepository: LogRepository,
    private readonly dataCatLogger: DataCatLogger
  ) {
    super(logRepository);
  }

  async logInDb(dto: LogDto) {
    this.dataCatLogger.log(dto);
  }
}

Nasza klasa adaptera spełnia typ bazowego loggera, z uwagi na fakt, że go rozszerza. Możemy więc spokojnie nadpisać konfigurację auto dependency injecta, jeżeli takowy stosujemy.

Z interfejsem będzie łatwiej

W bardziej przyziemnych sytuacjach, możemy spotkać się z podobnym problemem, ale z poprawnym użyciem interfejsu. Wtedy nasz adapter może zaimplementować wspólny interfejs, zamiast dziedziczyć po loggerze. Da nam to łatwość zmiany wstrzykiwanej zależności.

interface LoggerInterface {
  logInDb(dto: LogDto);
}

class Logger implements LoggerInterface {}

class SomeService {
  constructor(
   // ...
   private readonly logger: LoggerInterface
  ) {}

  // ...
}

class DataCatLoggerAdapter implements LoggerInterface {
  constructor(
    private readonly dataCatLogger: DataCatLogger
  ) {}

  async logInDb(dto: LogDto) {
    this.dataCatLogger.log(dto);
  }
}

Jeżeli zaszłaby potrzeba logowania i w bazie i u zewnętrznego dostawcy, możemy w naszym adapterze wstrzyknąć Logger, wywołać go, a następnie odpytać zew. usługę.

Podsumowanie

  • Wzorzec adapter pozwala na użycie dwóch niekompatybilnych ze sobą interfejsów.
  • Adapter pattern jest swoistego rodzaju przejściówką, umożliwiającą zastosowanie logiki domyślnie nieakceptowalnej w pewnych miejscach.
  • Stosowanie tego wzorca generuje dużą, niepotrzebną złożoność kodu wraz z każdym kolejnym adapterem.
Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również
  • Wzorzec strategia (strategy pattern)

    Jeżeli masz dość if-ologii w swoim kodzie, to konieczne sprawdź czym jest czynnościowy wzorzec projektowy strategia. Pozwala on mądrze obsługiwać różne scenariusze w procesie i jednocześnie być fancy pod względem zasad SOLID.

    Zobacz wpis

  • SOLID z przykładami w TypeScript

    SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, Dependency inversion principle), czyli pięć zasad programowania obiektowego, które każdy powinien przestrzegać.

    Zobacz wpis

  • Prawo Demeter (The Law of Demeter)

    Omawiam dzisiaj mało znaną zasadę, której stosowanie skutkuje posiadaniem łatwo utrzymywalnego i testowalnego kodu. Mowa o prawie demeter, które w najprostszym ujęciu zakłada, że obiekty powinny operować jedynie na najbliższym im otoczeniu.

    Zobacz wpis