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.
Spis treści
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:
- chcemy użyć klasy, która implementuje inny interfejs niż wymagany przez klasę klienta.
- stary system informatyczny wymaga w parametrze metody innego interfejsu, niż interfejs klasy, którą chcemy do niej przekazać.
- integrujemy się z klasą dostarczoną przez inny zespół i chcemy ją opakować w interfejs już istniejący w naszym systemie.
- chcemy ujednolicić kilka klas o różnych interfejsach, które spełniają podobne zadania
- chcemy odseparować warstwę domeny od interfejsu zewnętrznej biblioteki, tak aby nie polegać na interfejsie z zewnątrz, który może się zmienić.
- chcemy opakować klasę, która nie posiada żadnego interfejsu (np. klasa
HttpClient
w C# nie spełnia żadnego interfejsu, przez co bardzo ciężko ją testować i symulować jej zachowanie).
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.
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.
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:
- kierując się zasadą “Kompozycja ponad dziedziczenie” (ang. composition over inheritance) lepiej używać adaptera obiektowego, który jest oparty na kompozycji
jeżeli klasa bazowa jest zapieczętowana (ang. sealed), to nie możemy z niej dziedziczyć i musimy użyć adaptera obiektowego:
public sealed class KlasaAdaptowana { public void InnaOperacja() { } } public interface IInterfejsDocelowy { public void Operacja(); } public class Adapter : IInterfejsDocelowy { private KlasaAdaptowana _adaptowany; public void Operacja() { _adaptowany.InnaOperacja(); } }
jeżeli chcemy dziedziczyć po kilku klasach bazowych, to może ograniczać nas język. W C++ wielokrotne dziedziczenie jest możliwe, jednak w C# możemy dziedziczyć tylko po jednej klasie. W takim wypadku musimy użyć adaptera obiektowego:
public class KlasaAdaptowana1 { public void InnaOperacja() { } } public class KlasaAdaptowana2 { public void InnaOperacja() { } } public interface IInterfejsDocelowy { public void Operacja1(); public void Operacja2(); } public class Adapter : IInterfejsDocelowy { private KlasaAdaptowana1 _adaptowany1; private KlasaAdaptowana2 _adaptowany2; public void Operacja1() { _adaptowany1.InnaOperacja(); } public void Operacja2() { _adaptowany2.InnaOperacja(); } }
jeżeli kontrakt klienta jest klasą abstrakcyjną, a nie interfejsem, to ponownie może ograniczać nas język. W C# możemy dziedziczyć tylko po jednej klasie, więc będziemy zmuszeni użyć adaptera obiektowego:
public class KlasaAdaptowana { public void InnaOperacja() { } } abstract class KlasaDocelowa { public void Operacja(); } class Adapter : KlasaDocelowa { private KlasaAdaptowana _adaptowany; public void Operacja() { _adaptowany.InnaOperacja(); } }
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.
- adapter spełnia zasadę pojedynczej odpowiedzialności (ang.
single responsibility principle
) - adapter umożliwia dopasowanie niepasujących interfejsów i hermetyzuje logikę odpowiedzialną za ten proces wewnątrz siebie.
W przykładzie 1. tego artykułu adapter zawiera w sobie logikę zamiany numeru telefonu na numer konta bankowego. Dzięki temu, ten kod będzie znajdował się tylko w jednym miejscu (w adapterze), a nie będzie rozrzucony po całej aplikacji. - adapter spełnia zasadę otwarty/zamknięty (ang.
open/closed principle
) - użycie adaptera pozwala nam na rozbudowę istniejącego kodu bez wprowadzania jakichkolwiek zmian do tego kodu. W przykładzie 1. tego artykułu adapter umożliwił nam obsługę dodatkowego rodzaju płatności, bez konieczności modyfikacji istniejących metod. Pozwala osiągnąć to pomimo niezgodności interfejsów. - adapter może spełniać zasadę odwrócenia zależności (ang.
dependency inversion principle
) - implementacja adaptera może pomóc nam dostarczyć interfejs klasom, które nie posiadają żadnego interfejsu, a my nie możemy ich rozbudować (np. zewnętrzne biblioteki, klasy wbudowane w platformę programistyczną itp). W przykładzie 2. tego artykułu obudowaliśmy w adapter klasęHttpClient
i uzyskaliśmy dodatkowy interfejs polimorficzny. - adapter klasowy jest oparty na dziedziczeniu, a adapter obiektowy na kompozycji
- adapter obiektowy daje więcej możliwości zastosowania, jednak może być wolniejszy
- zachowanie adaptera obiektowego możemy zmieniać podczas działania programu, a adaptera klasowego nie
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