pprogramowanie;

// blog o programowaniu i branży IT

rss

Adapter

24 września 2022, kategoria: Wzorce projektowe
adapter

Adapter to strukturalny wzorzec projektowy pozwalający zmienić interfejs jednej klasy na inny. Jego podstawowym zadaniem jest rozwiązanie problemu niepasujących do siebie interfejsów.

Kiedy używać wzorca adapter?

Używając adaptera możemy zamienić interfejs istniejącej już klasy na taki, który jest nam aktualnie potrzebny. Dzięki temu, wiele klas o różnych interfejsach może ze sobą współpracować.

Przykładowe scenariusze użycia adaptera:

Wzorzec adapter można nazwać wzorcem ratunkowym - przeważnie używa się go w starych systemach, w których nie chcemy wprowadzać zbyt wielkich zmian. Tworząc nową aplikacje używanie adaptera powinno zapalić czerwoną lampkę. Tworząc aplikację od zera mamy cały wachlarz wzorców projektowych i strukturę aplikacji możemy dopasować do potrzeb.

Konstrukcja wzorca adapter

Adapter występuje w odmianie klasowej oraz obiektowej. Obydwa warianty rozwiązuje te same zalety, różnią się tylko sposobem implementacji.

Adapter klasowy

Adapter w wariancie klasowym używa dziedziczenia, a więc związanie następuje w momencie kompilacji programu. Posiada przez to pewne ograniczenia i jest mniej elastyczny.

adapter-klasowy-uml

Po przełożeniu powyższego diagramu UML na kod C# wyglądałby on następująco:

public class KlasaAdaptowana
{
    public void InnaOperacja()
    {
        Console.WriteLine("Tekst");
    }
}

public interface IInterfejsDocelowy
{
    public void Operacja();
}

public class Adapter : KlasaAdaptowana, IInterfejsDocelowy
{
    public void Operacja()
    {
        InnaOperacja();
    }
}

Adapter obiektowy

Adapter w wariancie obiektowym używa kompozycji, a więc związanie następuje w momencie działania programu.

adapter-obiektowy-uml

Po przełożeniu powyższego diagramu UML na kod C# wyglądałby on następująco:

public class KlasaAdaptowana
{
    public void InnaOperacja()
    {
        Console.WriteLine("Tekst");
    }
}

public interface IInterfejsDocelowy
{
    public void Operacja();
}

public class Adapter : IInterfejsDocelowy
{
    private KlasaAdaptowana _adaptowany;
    
    public void Operacja()
    {
        _adaptowany.InnaOperacja();
    }
}

Który adapter lepiej wybrać?

Ponieważ obydwa rodzaje adaptera realizują te same cele, użycie konkretnej implementacji zależy od aktualnych wymagań. Można wziąć pod uwagę następujące kwestie:

Konsekwencje stosowania wzorca adapter

Jak w przypadku wszystkich wzorców projektowych, adapter spełnia zasady czystego kodu. Dzięki jego użyciu kod jest łatwiejszy do testowania i rozbudowy.

Podobieństwo do innych wzorców

Adapter a dekorator

Wzorzec dekorator (ang. decorator pattern) służy do dynamicznego dodawania nowych funkcjonalności do istniejących obiektów. Jest to możliwe dzięki użyciu kompozycji, tak jak w przypadku adaptera obiektowego. Dekorator nie zmienia interfejsu obiektu, którego opakowuje. Specyficzna budowa wzorca pozwala także na wielokrotne dekorowanie tej samej instancji (rekurencyjna kompozycja), co nie występuje w przypadku adaptera.

Adapter a pełnomocnik

Wzorzec pełnomocnik (ang. proxy pattern) służy do tworzenia zastępcy (pełnomocnika) dla istniejącej klasy. Pełnomocnik nie zmienia interfejsu obiektu, którego zastępuje. W odróżnieniu od dekoratora, pełnomocnik nie powinien dodawać nowych funkcjonalności do obiektu ani zmieniać jego stanu. Może co najwyżej ograniczać dostęp do pewnych funkcjonalności. Ponadto, w odróżnieniu od dekoratora, nie pozwala na rekurencyjną kompozycję.

Adapter a fasada

Wzorzec fasada (ang. facade pattern) służy do upraszczania skomplikowanych interfejsów. Podobnie jak adapter obiektowy ten wzorzec bazuje na kompozycji. W przeciwieństwie do adaptera ani nie tworzy nowego interfejsu, ani nie bazuje na starym. W większości przypadków fasada to klasa z kilkoma metodami, która agreguje wewnątrz siebie inne klasy i interfejsy, oraz upraszcza znacząco ich użycie.

Przykłady

Przykład zastosowania 1

Stary system informatyczny posiada metodę ProcessPayment. Metoda ta przyjmuje obiekty implementujące interfejs IBankPayment. Chcesz rozszerzyć funkcjonalność systemu, tak aby obsługiwał płatności BLIK. Klasę obsługującą płatności BLIK dostarczyła zewnętrzna firma, więc nie masz wpływu na metody jakie posiada ani interfejsy jakie implementuje. Kod aplikacji wygląda następująco:

public interface IBankPayment
{
    int Amount();
    string BankAccount();
}

public class PaymentService
{
    public void ProcessPayment(IBankPayment payment)
    {
        // process payment
    }
}

Nowa klasa do płatności BLIK implementuje następujący interfejs:

public interface IMobilePayment
{
    int Amount();
    string PhoneNumber();
}

Aby dopasować interfejs IBankPayment do interfejsu IMobilePayment najlepiej skorzystać ze wzorca adapter. Zadaniem adaptera będzie umożliwienie współpracy dwóch różnych, niepasujących do siebie interfejsów. Adapter będzie także hermetyzował logikę odpowiedzialną za zamianę numeru telefonu na numer konta bankowego. Przykładowy kod adaptera:

public class MobileToBankPaymentAdapter : IBankPayment
{
    private readonly IMobilePayment _mobilePayment;

    public MobileToBankPaymentAdapter(IMobilePayment mobilePayment)
    {
        _mobilePayment = mobilePayment;
    }

    public int Amount() => _mobilePayment.Amount();

    public string BankAccount()
    {
        string bankAccount = "PL555555555555555555";
        // bankAccount = service.FindBankAccountByPhoneNumber(_mobilePayment.PhoneNumber);

        if (bankAccount == null) {
            throw new Exception("Could not map phone number to bank account!");
        }

        return bankAccount;
    }
}

Dzięki użyciu adaptera nie musimy zmieniać aktualnego kodu systemu ani dodawać do niego nowych metod. Dodatkowo, logika odpowiedzialna za zamianę numeru telefonu na numer konta bankowego jest oddzielona od domeny aplikacji. Przykładowe użycie adaptera wygląda następująco:

var paymentService = new PaymentService();

IBankPayment swiftPayment = new SwiftPayment(300, "PL000000000000000000");
IBankPayment blikPayment = new MobileToBankPaymentAdapter(new BlikPayment(100, "TEL 555-555-555"));

paymentService.ProcessPayment(swiftPayment);
paymentService.ProcessPayment(blikPayment);

Kod dostępny na github: https://github.com/p-programowanie/wzorce-projektowe/blob/master/adapter/adapter-obiektowy-1.cs
Kod dostępny na .NET Fiddle: https://dotnetfiddle.net/Ot6MC3

Przykład zastosowania 2

System informatyczny posiada serwis BonusService z metodą GetCustomerBonusValue. Metoda ta zwraca wielkość bonusu jaki można naliczyć klientowi i przyjmuje obiekt klienta jako parameter. Wewnątrz metody została użyta klasa HttpClient, która nie dziedziczy żadnego interfejsu. Serwis wygląda następująco:

public class BonusService : IBonusService
{
    private readonly HttpClient _httpClient;

    public BonusService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<int> GetCustomerBonusValue(Customer customer)
    {
        // more logic
    }

Chcesz napisać testy jednostkowe do metody GetCustomerBonusValue. Niestety, dużym utrudnieniem jest fakt, że klasa HttpClient pochodząca z przestrzeni nazw System.Net.Http nie dziedziczy żadnego interfejsu, przez co nie możesz w łatwy sposób symulować jej zachowania. Dobrym rozwiązaniem będzie opakowanie klasy HttpClient we wzorzec adapter, dzięki czemu będziesz mógł utworzyć własny interfejs, a następnie użyć go w klasie BonusService. Przykładowy adapter może wyglądać następująco:

public interface IHttpClient
{
    Task<HttpResponseMessage> GetAsync(string requestUri);
}

public class HttpClientAdapter : IHttpClient
{
    private readonly HttpClient _httpClient;

    public HttpClientAdapter()
    {
        _httpClient = new HttpClient();
    }

    public Task<HttpResponseMessage> GetAsync(string requestUri)
    {
        return _httpClient.GetAsync(requestUri);
    }
}

Dzięki takiemu prostemu zabiegowi, możesz zacząć używać interfejsu IHttpClient zamiast klasy HttpClient. Mimo, że oryginalna klasa nie dziedziczy z żadnego interfejsu, mogliśmy za pomocą adaptera dopasować ją do naszego nowego interfejs. Kod serwisu może wyglądać następująco:

public class BonusService : IBonusService
{
    private readonly IHttpClient _httpClient;

    public BonusService(IHttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<int> GetCustomerBonusValue(Customer customer)
    {
        // more logic
    }
}

Dzięki użyciu adaptera uzależniliśmy nasz serwis od abstrakcji spełniając 5. zasadę solid “odwrócenia zależności” (ang. dependency inversion principle). Kod można teraz bardzo prosto przetestować.

Kod dostępny na github: https://github.com/p-programowanie/wzorce-projektowe/blob/master/adapter/adapter-obiektowy-2.cs
Kod dostępny na .NET Fiddle: https://dotnetfiddle.net/P09IUb