🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Adapter jako fundament architektury Ports & Adapters (Hexagonal Architecture)

a close up of a red and white electrical device

Adapter to nie tylko GoF – to nazwa całej architektury

Większość artykułów o wzorcu Adapter kończy się na przykładzie „owijamy jedną klasę drugą klasą”. To prawda na poziomie mechaniki, ale pomija najważniejszą rzecz: Adapter jest literalnie drugim słowem w nazwie architektury Ports & Adapters (Hexagonal Architecture), którą realizujesz, pisząc projekty w Clean Architecture. To nie przypadek – cała warstwa Infrastructure w Twoim projekcie ASP.NET Core jest zbiorem adapterów.

W tym artykule pokazuję wzorzec od tej strony: nie jako pojedynczy przykład z podręcznika, tylko jako mechanizm, który pozwala Domain Layer nie wiedzieć nic o tym, że dane leżą w SQL Server, MongoDB czy pliku CSV.

Punkt wyjścia: Port zdefiniowany przez domenę

W Clean Architecture reguła zależności mówi jasno: warstwy zewnętrzne zależą od wewnętrznych, nigdy odwrotnie. Domain/Application definiuje port — interfejs wyrażony w języku domeny, bez żadnego śladu technologii bazodanowej:

// Warstwa Application — port zdefiniowany w języku domeny
public interface IOrderRepository
{
    Task<Order?> FindByIdAsync(OrderId id, CancellationToken ct);
    Task SaveAsync(Order order, CancellationToken ct);
}

Zwróć uwagę: brak DbContext, brak SqlConnection, brak niczego, co zdradzałoby technologię. To jest kontrakt, którego Domain oczekuje.

Adaptee: legacy system, którego nie kontrolujesz

Załóżmy realny scenariusz z projektu migracyjnego: firma ma 15-letni system ERP z własną, dziwaczną bazą danych, do której dostęp masz wyłącznie przez firmowe stored procedures zwracające płaskie, zdenormalizowane rekordy:

// Istniejący, legacy dostęp do danych — Adaptee
public class ErpOrderDataAccess
{
    public LegacyOrderRow GetOrderRow(int erpOrderNumber) { /* wywołanie sp_GetOrder */ }
    public void UpsertOrderRow(LegacyOrderRow row) { /* wywołanie sp_SaveOrder */ }
}

public class LegacyOrderRow
{
    public int ErpOrderNumber;
    public string CustomerCode;      // "K-000123" zamiast CustomerId (Guid)
    public decimal TotalNet;
    public decimal TotalGross;
    public string StatusFlag;        // "0","1","9" zamiast enum OrderStatus
}

Ten typ nigdy nie powinien przeciekać do Domain ani Application. StatusFlag == "9" to wiedza specyficzna dla ERP-a sprzed dekady – jeśli pozwolisz jej dotrzeć do logiki biznesowej, uzależniasz całą domenę od decyzji projektowych systemu, którego nie kontrolujesz.

Adapter jako Anti-Corruption Layer (DDD)

W terminologii Domain-Driven Design ten konkretny przypadek użycia Adaptera ma osobną nazwę: Anti-Corruption Layer (ACL). Cel jest identyczny jak w GoF, ale intencja jest podkreślona – chronisz model domenowy przed „skorumpowaniem” przez obcy model pojęciowy.

// Warstwa Infrastructure — Adapter pełniący rolę Anti-Corruption Layer
public sealed class ErpOrderRepositoryAdapter : IOrderRepository
{
    private readonly ErpOrderDataAccess _erpDataAccess;

    public ErpOrderRepositoryAdapter(ErpOrderDataAccess erpDataAccess)
    {
        _erpDataAccess = erpDataAccess;
    }

    public async Task<Order?> FindByIdAsync(OrderId id, CancellationToken ct)
    {
        var row = await Task.Run(() => _erpDataAccess.GetOrderRow(id.ToErpNumber()), ct);
        if (row is null) return null;

        return Order.Reconstitute(
            id: OrderId.FromErpNumber(row.ErpOrderNumber),
            customerId: CustomerId.FromErpCode(row.CustomerCode),
            total: Money.FromNetAndGross(row.TotalNet, row.TotalGross),
            status: MapStatus(row.StatusFlag));
    }

    public async Task SaveAsync(Order order, CancellationToken ct)
    {
        var row = new LegacyOrderRow
        {
            ErpOrderNumber = order.Id.ToErpNumber(),
            CustomerCode = order.CustomerId.ToErpCode(),
            TotalNet = order.Total.Net,
            TotalGross = order.Total.Gross,
            StatusFlag = MapStatus(order.Status)
        };
        await Task.Run(() => _erpDataAccess.UpsertOrderRow(row), ct);
    }

    private static OrderStatus MapStatus(string flag) => flag switch
    {
        "0" => OrderStatus.Draft,
        "1" => OrderStatus.Confirmed,
        "9" => OrderStatus.Cancelled,
        _ => throw new InvalidOperationException($"Nieznana flaga statusu ERP: {flag}")
    };

    private static string MapStatus(OrderStatus status) => status switch
    {
        OrderStatus.Draft => "0",
        OrderStatus.Confirmed => "1",
        OrderStatus.Cancelled => "9",
        _ => throw new ArgumentOutOfRangeException(nameof(status))
    };
}

Dlaczego to się opłaca architektonicznie — konsekwencja, nie ozdobnik

Kluczowa korzyść ujawnia się dopiero w scenariuszu zmiany, a nie w momencie napisania kodu. Załóżmy, że rok później firma migruje z legacy ERP na nowoczesne API REST. Ponieważ Order, IOrderRepository i cała logika biznesowa zależą wyłącznie od portu, zmiana polega na:

public sealed class RestApiOrderRepositoryAdapter : IOrderRepository
{
    private readonly HttpClient _httpClient;
    // ... implementacja tego samego portu IOrderRepository,
    // tłumacząca teraz JSON z API zamiast wierszy z ERP
}

i podmianie jednej linii w kompozycji zależności:

// Przed
builder.Services.AddScoped<IOrderRepository, ErpOrderRepositoryAdapter>();

// Po migracji
builder.Services.AddScoped<IOrderRepository, RestApiOrderRepositoryAdapter>();

Zero zmian w Domain, Application, kontrolerach, testach logiki biznesowej. To jest realny, mierzalny efekt zastosowania Adaptera na poziomie architektury — nie „elastyczność” w oderwaniu od kontekstu, tylko konkretny koszt zmiany zredukowany z „przepisz połowę systemu” do „napisz jedną nową klasę”.

Adapter vs Repository Pattern — częste nieporozumienie

Warto rozróżnić dwa pojęcia, które w praktyce często się mylą:

  • Repository Pattern opisuje rolę — kolekcjopodobny dostęp do agregatów domenowych (FindByIdAsync, SaveAsync).
  • Adapter opisuje mechanizm implementacji tej roli, gdy źródło danych ma niekompatybilny interfejs.

Jeśli implementujesz IOrderRepository bezpośrednio na EF Core (DbContext.Orders), technicznie to nadal jest adaptacja (ORM → port domenowy), ale różnica jest mniej odczuwalna, bo EF Core i tak operuje na encjach zbliżonych do modelu domenowego. Różnica staje się bardzo widoczna, gdy adaptowane źródło (legacy SP, zewnętrzne REST API, plik CSV) ma model pojęciowy fundamentalnie różny od Twojej domeny — i to jest sygnał, że mówisz o Adapterze/ACL, a nie o „zwykłej” implementacji repozytorium.

Kiedy NIE warto formalizować tego jako osobny wzorzec

Jeśli źródło danych jest pod Twoją pełną kontrolą (własna baza SQL zaprojektowana pod domenę, EF Core z encjami 1:1 do modelu domenowego), pełny ACL z ręcznym mapowaniem pól to niepotrzebny narzut. W takim przypadku wystarczy standardowa implementacja repozytorium na EF Core bez dodatkowej warstwy tłumaczącej — koszt utrzymania dodatkowego mapowania nie jest uzasadniony brakiem realnej niezgodności modeli.

Podsumowanie

Nie każde repozytorium wymaga pełnego ACL — stosuj go tam, gdzie model źródła danych realnie różni się pojęciowo od modelu domenowego, a nie jako domyślny narzut na każdą implementację repozytorium.

Adapter na poziomie architektonicznym to nie ciekawostka — to mechanizm, na którym opiera się cała warstwa Infrastructure w Clean Architecture / Ports & Adapters.

W DDD wariant Adaptera chroniący model domenowy przed obcym modelem pojęciowym ma własną nazwę: Anti-Corruption Layer.

Realna wartość: podmiana źródła danych (legacy ERP → REST API → inna baza) nie dotyka Domain, Application ani testów logiki biznesowej — zmienia się wyłącznie jedna klasa w Infrastructure i jedna linia w rejestracji DI.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

2 comments

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →