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




Polecane wpisy:

Wzorzec stan (state)

Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.

Sprawdź ten wpis

Wzorzec pełnomocnik (virtual proxy)

Wzorzec virtual proxy służy do tworzenia obiektów pośrednich, które nadzorują dostęp do obiektów dla których są pełnomocnikami. Dzięki temu, że pełnomocnik i klasa bazowa udostępniają jednakowy interfejs, możemy ukryć w pośredniku dodatkową logikę.

Sprawdź ten wpis

Wzorzec strategia (strategy pattern)

Jeżeli masz dość ifologii 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.

Sprawdź ten wpis

Wzorzec dekorator

Strukturalny wzorzec projektowy dekorator, to lekarstwo na problemy z mnogością różnych podtypów. Pozwala on na dynamiczne generowanie kolejnych typów obiektów bazujących na pierwotnym typie, bez konieczności deklarowania nowych, dedykowanych klas.

Sprawdź ten wpis

Wzorzec budowniczy (builder)

Wzorzec projektowy budowniczy służy do dynamicznej budowy obiektów. Pozwala na kreowanie różnych typów obiektu w zależności od przesłanych wymagań. Dzięki temu posiadamy centralny punkt odpowiedzialny jedynie za tworzenie wariacji danego typu.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.