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.

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.
