Wzorzec Adapter w praktyce. Jak łączyć niezgodne interfejsy?

Dwa adaptery wtyczek, jeden brytyjski, drugi europejski, ilustrujące adapter pattern.

Autor

Alex Jabłoński

Opublikowano

3 lis 2026

Spis treści

Integrujesz starą bibliotekę, zewnętrzne API albo kilka usług o podobnym przeznaczeniu, ale każda z nich oczekuje innego formatu danych i innych nazw metod? Właśnie w takich sytuacjach przydaje się wzorzec Adapter, który pozwala połączyć niezgodne interfejsy bez przebudowy całej aplikacji. Pokażę, jak działa, z czego się składa, kiedy warto go zastosować i jak napisać go w praktyce.

Najważniejsze informacje o wzorcu Adapter w jednym miejscu

  • Cel: połączenie klas lub usług, których interfejsy do siebie nie pasują.
  • Mechanizm: adapter tłumaczy wywołania i dane między kodem klienta a istniejącą klasą.
  • Najczęstsze zastosowanie: integracja API, kodu legacy i bibliotek zewnętrznych.
  • Preferowana odmiana: adapter obiektowy oparty na kompozycji, a nie dziedziczeniu.
  • Główne ryzyko: niepotrzebne zwiększenie liczby klas i ukrycie problemu z niejasnym kontraktem.

Diagram pokazuje, jak adapter pattern pozwala aplikacji

Po co w ogóle potrzebny jest adapter

Po angielsku spotkasz nazwę adapter pattern, natomiast w polskich materiałach najczęściej mówi się po prostu o wzorcu Adapter. Jego zadanie jest proste: istniejący obiekt ma działać w miejscu, w którym oczekiwany jest inny interfejs.

Dobrym przykładem jest aplikacja sklepu internetowego. Kod zamówień może oczekiwać metody pay(amount), podczas gdy zewnętrzny operator płatności udostępnia metodę createTransaction(valueInCents, currencyCode). Obie operacje prowadzą do podobnego celu, ale różnią się nazwami, parametrami i sposobem zwracania wyniku.

Zamiast zmieniać kod zamówień albo modyfikować bibliotekę operatora, dodaję klasę pośrednią. Ona przyjmuje wywołanie zgodne z interfejsem aplikacji, przelicza dane i deleguje pracę do usługi zewnętrznej.

Największa korzyść polega na odizolowaniu różnic w jednym miejscu. Gdy dostawca zmieni format odpowiedzi, poprawiam adapter, a nie wszystkie kontrolery, serwisy i testy, które korzystają z płatności.

Adapter nie zmienia źródłowej klasy

Wzorzec dobrze sprawdza się wtedy, gdy nie mam dostępu do kodu źródłowego, nie chcę naruszać stabilnej biblioteki albo jej zmiana mogłaby zepsuć inne fragmenty systemu. W praktyce często dotyczy to starego kodu, SDK dostawcy i systemów zewnętrznych.

Adapter nie musi wykonywać skomplikowanej logiki biznesowej. Jego rolą jest głównie tłumaczenie kontraktu, nazw, typów danych, jednostek i kolejności argumentów. Jeśli zaczyna podejmować decyzje biznesowe, prawdopodobnie trafia do niego zbyt dużo odpowiedzialności.

Jak działa ten wzorzec krok po kroku

W typowej implementacji występują cztery elementy. Klient korzysta wyłącznie z interfejsu docelowego, adapter implementuje ten interfejs, a opakowana usługa udostępnia własny, niepasujący kontrakt.

Element Rola
Klient Kod aplikacji, który chce wykonać operację.
Target Interfejs oczekiwany przez klienta.
Adapter Obiekt tłumaczący wywołania i dane.
Adaptee Istniejąca usługa lub klasa z innym interfejsem.

Przepływ wygląda następująco: klient wywołuje metodę interfejsu docelowego, adapter odbiera żądanie, konwertuje argumenty i przekazuje je do opakowanej klasy. Następnie tłumaczy odpowiedź na format, który rozumie klient.

To ważne rozdzielenie sprawia, że kod klienta nie musi wiedzieć, czy pod spodem działa biblioteka JavaScript, usługa REST, stary moduł PHP czy lokalna klasa. Z jego perspektywy istnieje tylko jeden stabilny kontrakt.

Przykład w TypeScript

Poniższy przykład pokazuje integrację aplikacji z dostawcą płatności, którego API używa innych nazw i jednostek niż reszta systemu.

type PaymentResult = {
  transactionId: string;
  accepted: boolean;
};

interface PaymentGateway {
  pay(amountInPln: number): Promise;
}

class ExternalPaymentClient {
  async createTransaction(
    valueInGrosze: number,
    currency: string
  ): Promise<{ id: string; status: string }> {
    return {
      id: "tx-123",
      status: "accepted"
    };
  }
}

class ExternalPaymentAdapter implements PaymentGateway {
  constructor(private client: ExternalPaymentClient) {}

  async pay(amountInPln: number): Promise {
    const response = await this.client.createTransaction(
      Math.round(amountInPln * 100),
      "PLN"
    );

    return {
      transactionId: response.id,
      accepted: response.status === "accepted"
    };
  }
}

const gateway: PaymentGateway =
  new ExternalPaymentAdapter(new ExternalPaymentClient());

await gateway.pay(149.99);

Adapter wykonuje tu trzy konkretne czynności. Przelicza złote na grosze, ustawia kod waluty i mapuje odpowiedź dostawcy na typ używany przez aplikację. Dzięki temu reszta systemu nie musi znać specyfiki zewnętrznego klienta.

W praktyce właśnie takie drobne konwersje często są najbardziej wartościowe. Nie wyglądają efektownie, ale chronią kod domenowy przed przeciekaniem szczegółów integracji do wielu miejsc.

Dwie odmiany i rozsądny wybór między nimi

Wzorzec można zrealizować przez kompozycję albo dziedziczenie. W nowoczesnych aplikacjach webowych zazwyczaj wybieram pierwszą możliwość, ponieważ daje większą elastyczność i nie wymaga wielokrotnego dziedziczenia.

Rodzaj Jak działa Kiedy ma sens Ograniczenie
Adapter obiektowy Przechowuje instancję opakowywanej klasy i deleguje do niej wywołania. Integracje z bibliotekami, API i kodem legacy. Dodaje dodatkową warstwę delegowania.
Adapter klasowy Wykorzystuje dziedziczenie adaptera po interfejsie docelowym i klasie źródłowej. Wybrane języki i proste hierarchie klas. Wymaga możliwości wielokrotnego dziedziczenia albo podobnego mechanizmu.

Adapter obiektowy jest zwykle bezpieczniejszy, bo pozwala podmienić opakowywaną usługę w czasie działania lub w testach. Mogę przekazać atrapę, innego dostawcę albo nową wersję biblioteki bez zmiany dziedziczenia.

Adapter klasowy bywa krótszy, ale mocniej wiąże rozwiązanie z konkretną hierarchią. W TypeScript, Javie i C# częściej stosuje się kompozycję, ponieważ klasy nie mogą dziedziczyć po wielu klasach jednocześnie albo takie dziedziczenie szybko komplikuje projekt.

Gdzie Adapter sprawdza się najlepiej

Najczęściej używam go na granicach systemu, czyli tam, gdzie aplikacja styka się z cudzym kodem. To naturalne miejsce na izolowanie zmian, ponieważ zewnętrzne kontrakty nie zawsze są zaprojektowane pod potrzeby naszej domeny.

Integracja kilku dostawców

Sklep może obsługiwać płatności kartą, szybki przelew i płatność odroczoną. Każdy operator zwraca inne statusy, ale aplikacja może potrzebować tylko trzech stanów: zaakceptowana, odrzucona i oczekująca. Osobny adapter dla każdego dostawcy ujednolica te różnice.

Modernizacja kodu legacy

Stary moduł może udostępniać metodę get_customer_data(), podczas gdy nowa część aplikacji używa findCustomer(). Adapter pozwala rozpocząć migrację etapami, bez przepisywania całego systemu w jednym dużym wydaniu.

Zmiana formatu danych

Adapter może konwertować XML na obiekt domenowy, jednostki imperialne na metryczne albo odpowiedź z REST API na model używany przez frontend. Nie chodzi wyłącznie o nazwy metod. Równie ważne są typy, jednostki, wartości domyślne i obsługa błędów.

Przeczytaj również: Pub/Sub - Jak działa, kiedy używać i unikać błędów?

Testowanie i wymiana implementacji

Jeśli kod zależy od interfejsu, w testach mogę podstawić prostą implementację zamiast prawdziwego klienta zewnętrznego. To ogranicza liczbę testów wymagających sieci i sprawia, że testy jednostkowe są szybsze oraz bardziej przewidywalne.

Nie tworzyłbym jednak adaptera dla każdej pojedynczej funkcji. Gdy różnica jest banalna, zwykła funkcja mapująca może być czytelniejsza. Wzorzec zaczyna przynosić realną wartość wtedy, gdy integracja ma własny kontrakt, kilka operacji lub prawdopodobnie będzie się zmieniać.

Adapter a fasada, dekorator i strategia

Te wzorce bywają mylone, bo wszystkie mogą opakowywać inne obiekty. Różni je jednak cel, a ten cel najlepiej traktować jako główne kryterium wyboru.

Wzorzec Główne pytanie Co robi
Adapter Jak dopasować niezgodny interfejs? Tłumaczy jeden kontrakt na drugi.
Fasada Jak uprościć skomplikowany moduł? Udostępnia prostszy punkt wejścia do wielu operacji.
Dekorator Jak dodać zachowanie bez zmiany klasy? Rozszerza funkcję, zachowując ten sam interfejs.
Strategia Jak wymieniać sposób wykonania algorytmu? Ukrywa kilka wariantów zachowania za wspólnym kontraktem.

Jeżeli interfejs już pasuje, ale chcesz dodać logowanie, cache albo pomiar czasu, prawdopodobnie potrzebujesz dekoratora. Jeżeli chcesz ukryć kilka usług za jedną metodą, lepsza będzie fasada. Adapter wybieram wtedy, gdy podstawowy problem brzmi: „te dwa elementy nie potrafią się ze sobą bezpośrednio porozumieć”.

Jak wdrożyć go bez tworzenia zbędnej warstwy

Zaczynam od opisania interfejsu, którego naprawdę potrzebuje kod aplikacji. Nie kopiuję bezrefleksyjnie całego API dostawcy. Im węższy i bardziej domenowy kontrakt, tym łatwiej wymienić implementację.

  1. Zidentyfikuj klasę lub usługę, której nie możesz albo nie chcesz zmieniać.
  2. Spisz operacje potrzebne klientowi, bez szczegółów konkretnego dostawcy.
  3. Zdefiniuj interfejs docelowy.
  4. Utwórz adapter, który przechowuje zależność do opakowywanej usługi.
  5. Dodaj mapowanie argumentów, wyników i wyjątków.
  6. Podłącz klienta do interfejsu, a nie do konkretnej klasy adaptera.
  7. Przetestuj poprawne dane, błędy i nietypowe przypadki.

Szczególną uwagę poświęcam błędom. Gdy dostawca zwraca cztery rodzaje odrzucenia, a aplikacja rozpoznaje tylko jeden, adapter nie powinien po cichu gubić informacji. Lepiej jawnie ustalić mapowanie i zachować istotny kontekst diagnostyczny.

Drugim częstym problemem jest zbyt szeroki adapter. Jeśli klasa zaczyna zarządzać zamówieniami, autoryzacją, retry i logiką rabatową, przestaje być zwykłym tłumaczem. Wtedy rozdzielam integrację od logiki biznesowej i zostawiam adapterowi tylko to, co dotyczy zgodności interfejsów.

Trzeba też uważać na ukryte konwersje. Przeliczanie groszy na złote jest proste, ale zmiana stref czasowych, zaokrągleń lub semantyki statusów może powodować trudne błędy. Takie reguły powinny mieć osobne testy, nawet jeśli sam adapter wydaje się niewielki.

Gdy adapter rozwiązuje problem, a nie go maskuje

Najlepszy moment na użycie tego wzorca pojawia się wtedy, gdy różnica między interfejsami jest lokalna, zrozumiała i możliwa do przetestowania. Wtedy adapter ogranicza sprzężenie, ułatwia wymianę dostawcy i chroni kod aplikacji przed zmianami na zewnątrz.

Nie traktuję go jednak jako obowiązkowej ceremonii architektonicznej. Przy jednej prostej funkcji wystarczy mapper, a przy projektowaniu dwóch nowych modułów od zera lepiej od razu ustalić wspólny kontrakt niż później go naprawiać.

Najprostsza reguła brzmi więc tak: jeśli istniejąca klasa jest użyteczna, ale mówi językiem, którego reszta systemu nie rozumie, dodaj warstwę tłumaczącą. Dobry adapter jest mały, jawny i łatwy do usunięcia, gdy zewnętrzna zależność przestanie być potrzebna.

FAQ - Najczęstsze pytania

Adapter sprawdza się, gdy zewnętrzna usługa ma inne nazwy metod, parametry, typy danych lub format odpowiedzi niż aplikacja. Izoluje te różnice w jednym miejscu, dzięki czemu zmiana dostawcy lub jego API nie wymaga modyfikowania kontrolerów i serwisów.

Adapter może przeliczyć kwotę ze złotych na grosze, ustawić kod waluty PLN i zamienić odpowiedź dostawcy na model używany przez aplikację. Przykładowo status „accepted” może zostać zmapowany na wartość accepted w polu accepted, a identyfikator id na transactionId.

Adapter obiektowy przechowuje instancję opakowywanej klasy i deleguje do niej wywołania. Adapter klasowy wykorzystuje dziedziczenie, ale jest silniej związany z hierarchią klas. W TypeScript, Javie i C# zwykle lepsza jest kompozycja, ponieważ ułatwia podmianę zależności w testach i wymianę dostawcy.

Adapter powinien tłumaczyć kontrakt, argumenty, wyniki, jednostki i błędy. Nie powinien zarządzać zamówieniami, rabatami, autoryzacją ani retry. Gdy takie odpowiedzialności się pojawiają, warto rozdzielić integrację od logiki biznesowej i pozostawić adapterowi wyłącznie zgodność interfejsów.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

adapter kompozycja api typescript legacy

Udostępnij artykuł

Alex Jabłoński

Alex Jabłoński

Nazywam się Alex Jabłoński i od 14 lat zajmuję się programowaniem webowym. Moja przygoda z tą dziedziną zaczęła się od prostych projektów, które stopniowo przerodziły się w pasję do tworzenia złożonych aplikacji internetowych. Interesuje mnie nie tylko kodowanie, ale także dzielenie się wiedzą, co skłoniło mnie do pisania artykułów, które pomagają innym zrozumieć zasady programowania. W swoich tekstach koncentruję się na podstawach, ale także na bardziej zaawansowanych zagadnieniach, starając się w przystępny sposób wyjaśnić trudne tematy. Przy pisaniu zwracam szczególną uwagę na aktualność informacji oraz ich zrozumiałość. Regularnie śledzę nowinki w branży, porównuję różne podejścia i staram się uprościć skomplikowane koncepcje, aby były dostępne dla każdego, niezależnie od poziomu zaawansowania. Moim celem jest dostarczenie wartościowych treści, które pomogą czytelnikom w ich drodze do kariery w programowaniu webowym.

Napisz komentarz