Adapter pattern w C# - jak łączyć niezgodne interfejsy?

Przemysław Kwiatkowski 16 czerwca 2026
Diagram ukazuje strukturę wzorca adapter pattern. Klient nie może użyć Adaptee z powodu niekompatybilnego interfejsu.

Spis treści

Gdy nowy moduł ma korzystać ze starej biblioteki, zewnętrznego SDK albo usługi o innym kontrakcie, problemem często nie jest sama funkcjonalność, lecz sposób komunikacji. Adapter pattern pozwala połączyć takie elementy bez przepisywania istniejącego kodu. Pokażę, jak działa ten wzorzec, jak zaimplementować go w C#, gdzie sprawdza się w .NET oraz kiedy jego użycie zaczyna komplikować architekturę.

Adapter zamienia niepasujący interfejs w użyteczny kontrakt

  • Cel: połączenie obiektów, których interfejsy nie są ze sobą zgodne.
  • Najczęstsza forma: adapter oparty na kompozycji i delegowaniu wywołań.
  • Korzyść: kod klienta nie zna szczegółów legacy systemu ani zewnętrznej biblioteki.
  • .NET: wzorzec dobrze współpracuje z interfejsami, dependency injection i testami jednostkowymi.
  • Ograniczenie: adapter tłumaczy kontrakt, ale nie naprawia złego modelu domenowego ani słabej jakości danych.

Adapter pattern łączy dwa różne kontrakty

Wzorzec Adapter jest wzorcem strukturalnym. Jego zadaniem jest sprawienie, aby istniejąca klasa udawała przed klientem inny interfejs, którego ten klient oczekuje. Oryginalny kod pozostaje bez zmian, a różnicę między kontraktami obsługuje dodatkowa klasa.

Najłatwiej wyobrazić go sobie jak przejściówkę do gniazdka. Urządzenie nie musi wiedzieć, jaki standard obowiązuje w ścianie. Otrzymuje odpowiednie napięcie i może działać. W kodzie klient korzysta z własnego interfejsu, a adapter tłumaczy jego metody na wywołania obiektu, który naprawdę wykonuje pracę.

W typowym układzie występują cztery elementy:

  • Client korzysta z oczekiwanego interfejsu.
  • Target definiuje kontrakt wymagany przez klienta.
  • Adapter implementuje ten kontrakt i tłumaczy wywołania.
  • Adaptee zawiera właściwą funkcjonalność, ale udostępnia ją w innej formie.

Istotne jest to, że klient zna wyłącznie interfejs docelowy. Dzięki temu można wymienić bibliotekę, dostawcę albo sposób komunikacji bez zmiany logiki biznesowej. W praktyce właśnie ta izolacja zależności jest zwykle ważniejsza niż sam fakt zastosowania wzorca.

Diagram ukazuje strukturę wzorca adapter pattern: Client, Adapter, ConcreteAdapter i Adaptee. Client nie może użyć Adaptee bezpośrednio.

Jak zbudować adapter w C# na realnym przykładzie

Załóżmy, że aplikacja wysyła powiadomienia SMS. Kod biznesowy powinien pracować z prostym interfejsem, ale dostawca udostępnia klasę o własnych nazwach metod i parametrach.

public interface IMessageSender
{
    Task SendAsync(
        string recipient,
        string message,
        CancellationToken cancellationToken = default);
}

To jest kontrakt, który interesuje aplikację. Nie ma w nim informacji o konkretnym operatorze, formacie żądania ani nazwie biblioteki. Zewnętrzny klient może wyglądać zupełnie inaczej:

public sealed class LegacySmsClient
{
    public Task DispatchMessageAsync(
        string phoneNumber,
        string content,
        CancellationToken cancellationToken = default)
    {
        // Wywołanie zewnętrznego dostawcy
        return Task.CompletedTask;
    }
}

Adapter łączy oba światy. Przechowuje instancję klasy dostawcy i deleguje do niej wywołanie, jednocześnie dopasowując nazwy parametrów oraz strukturę metody.

public sealed class SmsMessageSenderAdapter : IMessageSender
{
    private readonly LegacySmsClient _client;

    public SmsMessageSenderAdapter(LegacySmsClient client)
    {
        _client = client;
    }

    public Task SendAsync(
        string recipient,
        string message,
        CancellationToken cancellationToken = default)
    {
        return _client.DispatchMessageAsync(
            recipient,
            message,
            cancellationToken);
    }
}

Kod aplikacji może teraz zależeć od IMessageSender, a nie od biblioteki dostawcy. W ASP.NET Core rejestracja w kontenerze DI może wyglądać tak:

builder.Services.AddSingleton();
builder.Services.AddScoped();

W bardziej rozbudowanym adapterze można również mapować modele, normalizować błędy i tłumaczyć kody odpowiedzi. Trzeba jednak pilnować granicy. Jeśli adapter zaczyna zawierać całą logikę biznesową, przestaje być prostą warstwą tłumaczącą i staje się trudniejszym do utrzymania serwisem integracyjnym.

Adapter powinien ukrywać szczegóły techniczne

Dobry adapter nie wypuszcza na zewnątrz typów dostawcy. Jeżeli interfejs aplikacji zaczyna zwracać klasy z konkretnego SDK, zależność przenika wyżej i główna korzyść wzorca znika. Sam staram się trzymać w adapterze konwersję modeli, obsługę błędów technicznych i delegowanie, a reguły biznesowe zostawiam poza nim.

Kompozycja czy dziedziczenie w adapterze

Wyróżnia się dwa klasyczne warianty. Adapter obiektowy korzysta z kompozycji, czyli przechowuje obiekt dopasowywanej klasy. Adapter klasowy opiera się na dziedziczeniu. W nowoczesnym C# najczęściej wybieram pierwszy wariant, ponieważ daje większą elastyczność i mniejsze powiązanie z implementacją.

Wariant Mechanizm Zalety Ograniczenia
Adapter obiektowy Kompozycja i delegowanie Łatwa podmiana zależności, dobra współpraca z DI i testami Trzeba jawnie przekazywać wywołania
Adapter klasowy Dziedziczenie po klasie adaptee Może wymagać mniej kodu w prostym przypadku Ograniczenie pojedynczym dziedziczeniem i silniejsze sprzężenie

Wariant obiektowy dobrze pasuje do sytuacji, w której zależność pochodzi z zewnętrznego pakietu albo jest dostarczana przez kontener. Można łatwo podmienić ją na mock lub fake podczas testów. Dziedziczenie ma sens głównie wtedy, gdy biblioteka została zaprojektowana do rozszerzania, a adapter rzeczywiście musi korzystać z chronionych elementów klasy bazowej.

W C# nie trzeba budować rozbudowanej hierarchii, żeby zastosować ten wzorzec. Zwykła klasa implementująca interfejs i posiadająca prywatne pole z obiektem zależności w zupełności wystarczy. To prostota kompozycji sprawia, że ten wariant wygrywa w większości aplikacji .NET.

Gdzie adapter przydaje się w .NET i Azure

Najbardziej praktyczne zastosowania pojawiają się na granicach systemu. Tam zwykle spotykają się różne style API, modele danych i sposoby obsługi błędów. Adapter pozwala zostawić te różnice przy brzegu aplikacji, zamiast rozprowadzać je po kontrolerach i logice domenowej.

Integracje z zewnętrznymi usługami

Różni dostawcy płatności, poczty, SMS-ów czy przechowywania plików często oferują podobną funkcję, ale pod innymi nazwami. Własny interfejs, na przykład IPaymentGateway, pozwala aplikacji operować na jednym modelu, podczas gdy osobny adapter obsługuje każdego dostawcę.

To szczególnie przydatne, gdy system ma wspierać kilku operatorów albo zachować możliwość migracji. Adapter nie usuwa kosztu integracji, ale sprawia, że koszt jest skupiony w jednym miejscu, a nie rozsiany po całym kodzie.

Legacy code i biblioteki bez wygodnego API

Starsza biblioteka może być stabilna, lecz niewygodna w użyciu. Zamiast przepisywać działający moduł, można opakować go własnym interfejsem. Takie podejście często sprawdza się przy migracji aplikacji, integracji z COM albo stopniowej wymianie komponentów.

W praktyce adapter bywa też bezpieczną warstwą przejściową. Najpierw aplikacja korzysta z niego przy użyciu starego komponentu, a później można podmienić implementację bez zmiany kodu wywołującego. To dobry sposób na migrację etapami, pod warunkiem że adapter nie staje się miejscem wiecznego odkładania porządków.

Przeczytaj również: Mediator w C# bez chaosu. Handlery, pipeline i dobre praktyki

Azure, AI i wiele implementacji jednego kontraktu

W aplikacjach korzystających z Azure lub usług AI często pojawia się potrzeba ujednolicenia różnych klientów SDK. Jeden adapter może tłumaczyć model używany przez aplikację na format wymagany przez konkretną usługę, a inny może obsługiwać lokalny model testowy.

Dzięki temu kod domenowy nie musi wiedzieć, czy odpowiedź pochodzi z chmury, lokalnego serwera czy atrapowej implementacji. Trzeba tylko uważać, aby nie ukrywać istotnych różnic jakościowych, takich jak limity, opóźnienia, koszty tokenów czy obsługiwane funkcje. Ujednolicony interfejs nie oznacza identycznego zachowania wszystkich dostawców.

Adapter, Decorator i Facade rozwiązują różne problemy

Te wzorce bywają mylone, ponieważ wszystkie mogą wyglądać jak klasa opakowująca inną klasę. Różni je przede wszystkim intencja. Adapter zmienia interfejs, Decorator dodaje zachowanie, a Facade upraszcza dostęp do większego podsystemu.

Wzorzec Główne pytanie Typowy efekt
Adapter Jak dopasować niezgodny kontrakt? Klient otrzymuje interfejs, którego oczekuje
Decorator Jak dodać zachowanie bez modyfikowania klasy? Dochodzi logowanie, cache, retry albo autoryzacja
Facade Jak uprościć dostęp do wielu klas? Powstaje wygodniejszy punkt wejścia do podsystemu

Jeżeli opakowujesz klienta HTTP, aby dodać retry i logowanie, prawdopodobnie tworzysz dekorator. Jeżeli tłumaczysz metodę DispatchMessageAsync na SendAsync, korzystasz z adaptera. Jeżeli ukrywasz pięć usług za jedną metodą CreateOrder, bliżej temu do fasady.

Typowe błędy i sytuacje, w których lepiej odpuścić

Najczęstszy błąd polega na tworzeniu adaptera tylko dlatego, że wzorzec dobrze wygląda na diagramie. Jeżeli obie klasy mają już zgodny kontrakt, dodatkowa warstwa niczego nie poprawia. Zyskujemy wtedy więcej plików, ale nie większą elastyczność.

Uważam też, że adapter nie powinien maskować problemów z danymi. Gdy jedna usługa zwraca kwoty w groszach, a druga w złotówkach, samo przemianowanie pola nie wystarczy. Trzeba jasno opisać jednostki, strefy czasowe, kodowanie i wartości opcjonalne, a konwersję objąć testami.

  • Nie przenoś typów zewnętrznego SDK do interfejsu domenowego.
  • Nie ukrywaj po cichu błędów, których klient powinien być świadomy.
  • Nie mieszaj tłumaczenia kontraktu z rozbudowaną orkiestracją procesu.
  • Nie twórz jednego ogromnego adaptera dla kilku niezależnych usług.
  • Testuj zarówno poprawne mapowanie, jak i błędne dane wejściowe.

Jeżeli różnice między systemami są bardzo duże, adapter może być tylko cienką fasadą dla większego modułu antykorupcyjnego. Taka warstwa chroni model domenowy przed pojęciami z zewnętrznego systemu, ale wymaga już świadomego projektowania, mapowania i monitorowania. W prostych integracjach wystarczy zwykły adapter, w złożonych potrzebna jest wyraźna granica architektoniczna.

Jak rozpoznać dobry moment na zastosowanie adaptera

Najprostszy test jest praktyczny. Jeżeli kod klienta zaczyna zawierać warunki zależne od konkretnego dostawcy, konwersje jego modeli albo komentarze wyjaśniające dziwne nazwy metod, prawdopodobnie potrzebujesz osobnej warstwy dopasowującej.

Dobry adapter ma zwykle jeden czytelny powód istnienia. Ukrywa zewnętrzny kontrakt, mapuje dane, przekazuje wywołania i jasno komunikuje błędy. Gdy te zadania są zamknięte w jednej klasie, wymiana biblioteki oraz testowanie aplikacji stają się wyraźnie prostsze.

Nie traktuję tego wzorca jako obowiązkowego elementu każdej integracji. Stosuję go wtedy, gdy różnica między interfejsami jest realna, zależność może się zmienić albo chcę ochronić kod domenowy przed technicznymi szczegółami. W takiej sytuacji niewielka klasa pośrednia potrafi oszczędzić wiele pracy podczas kolejnej zmiany dostawcy, migracji lub rozwoju systemu.

FAQ - Najczęstsze pytania

Adapter to strukturalny wzorzec projektowy, który dopasowuje niezgodny interfejs do kontraktu oczekiwanego przez klienta. W typowym układzie występują Client, Target, Adapter i Adaptee. Klient zna tylko interfejs Target, a adapter tłumaczy jego wywołania na metody istniejącego obiektu.

Najpierw należy zdefiniować własny interfejs, na przykład IMessageSender z metodą SendAsync. Adapter implementuje ten interfejs, przechowuje instancję LegacySmsClient i deleguje wywołanie do DispatchMessageAsync, mapując nazwy oraz parametry. W ASP.NET Core można zarejestrować dostawcę jako usługę i adapter jako implementację IMessageSender w kontenerze DI.

Adapter obiektowy oparty na kompozycji jest zwykle lepszy w nowoczesnym C#, ponieważ ułatwia podmianę zależności, współpracę z DI oraz testowanie za pomocą mocków lub fake'ów. Adapter klasowy może mieć mniej kodu, ale silniej wiąże rozwiązanie z klasą bazową i podlega ograniczeniu pojedynczego dziedziczenia. Dziedziczenie ma sens głównie wtedy, gdy biblioteka jest przeznaczona do rozszerzania.

Adapter zmienia interfejs, dekorator dodaje zachowanie, takie jak retry, cache lub logowanie, a fasada upraszcza dostęp do większego podsystemu. Nie warto tworzyć adaptera, gdy istniejące kontrakty są już zgodne, ponieważ dodatkowa warstwa nie zapewni wtedy realnej korzyści. Adapter nie powinien też zawierać całej logiki biznesowej ani ukrywać istotnych błędów i różnic między dostawcami.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

adapter
c#
.net
kompozycja
wstrzykiwanie zależności
Autor Przemysław Kwiatkowski
Przemysław Kwiatkowski
Jestem Przemysław i od 15 lat zajmuję się programowaniem .NET, chmurą Azure oraz sztuczną inteligencją. Moja przygoda z tymi technologiami zaczęła się od fascynacji możliwościami, jakie dają, a z czasem przerodziła się w pasję do tworzenia rozwiązań, które realnie wpływają na pracę i życie ludzi. Na kursdotnet.pl staram się dzielić się swoją wiedzą w sposób przystępny, tłumacząc złożone zagadnienia i pomagając zrozumieć, jak te dynamicznie rozwijające się obszary IT mogą być wykorzystane w praktyce. Dokładam wszelkich starań, aby prezentowane przeze mnie materiały były rzetelne, aktualne i oparte na sprawdzonych źródłach, a także aby uporządkować wiedzę w sposób ułatwiający jej przyswojenie.

Udostępnij artykuł

Napisz komentarz