Adapter


Adapter

Adapter znany też jako: Opakowanie, Nakładka, Wrapper. Adapter jest strukturalnym wzorcem projektowym pozwalającym na współdziałanie ze sobą obiektów o niekompatybilnych interfejsach.

Cel

Adapter konwertuje interfejs klasy na inny interfejs, którego oczekują klienci, pozwala klasom współpracować ze sobą, co nie byłoby możliwe z powodu niezgodnych interfejsów. Istnieją dwie odmiany tego wzorca, z których pierwsza to wzorzec Adapter Obiektów a druga to Adapter Klas.

Problem

Adapter Obiektów. Wyobraź sobie, że musisz zintegrować się z zewnętrznym systemem, który udostępnia dane Klienta (Customer).

Możemy na przykład tworzyć aplikację, która musi komunikować się z interfejsem API, który udostępnia dane tego klienta. Możemy też po prostu rozmawiać z inną warstwą aplikacji, która działa na innym modelu niż ten, którego ostatecznie potrzebujemy. W naszym przypadku załóżmy, że mamy klienta z zewnętrznego systemu (CustomerFromExternalSystem) z Imieniem, Nazwiskiem, Miastem, Ulicą, Numerem, Emailem i Telefonem (Name, Surname, City, Street, Number, Email, Phone).

Ta klasa CustomerFromExternalSystem jest udostępniana przez klasę system zewnętrzny (ExternalSystem), która zawiera, jedną metodę pobierz klienta (GetCustomer), która zwraca naszego klienta z systemu zewnętrznego. Nie możemy pracować z tym klientem z klasy ExternalSystem.

Nasz system potrzebuje klasy Customer, która składa się z właściwości imieNazwisko, adres i kontakt (FullName, Address, Contact). Czyli musimy jakoś przekonwertować ten CustomerFromExternalSystem na Customer.

Rozwiązanie

Jednym ze sposobów na to jest Adapter.

Możemy stworzyć nową klasę, która jest zgodna z określonym interfejsem i która odwołuje się do ExternalSystem. Po otrzymaniu wyniku z wywołania GetCustomer na poziomie systemu zewnętrznego konwertuje go na obiekt klienta (Customer) i zwraca go. To opakowanie jest tym, co nazwalibyśmy adapterem klienta (CustomerAdapter), oczywiście dostarczamy również interfejs (ICustomerAdapter), którego musi implementować nasz adapter CustomerAdapter.

Następnie klient wywołuje GetCustomer na adapterze ICustomerAdapter. Z mapujmy to na strukturę wzorca Adaptera Obiektów.

Struktura

Nasze dwie klasy, CustomerFromExternalSystem i Customer, są prostymi klasami pomocniczymi, które pomogły nam opisać wzorzec. Nie są częścią struktury wzorca.

Nasz system zewnętrzny, który zawiera metodę GetCustomer, która zwraca CustomerFromExternalSystem, czyli tak zwany adaptee, czyli rzecz, która wymaga adaptacji. Definiuje to w ten sposób istniejący interfejs, który należy dostosować. Tak więc na naszym diagramie przełączamy ExternalSystem na Adaptee (adaptację).

Interfejs ICustomerAdapter nosi nazwę Target. Innymi słowy, rzecz, do której musimy dostosować adaptowanego. Definiuje interfejs specyficzny dla domeny, z którym współpracuje klient. Dlatego przełączamy ICustomerAdapter na Target.

Klient pracuje z obiektami zgodnymi z interfejsem Target. Implementacja tego celu, CustomerAdapter, nosi nazwę Adapter lub, jeśli chcesz, opakowanie, które dostosowuje adaptację do celu. Zmieniamy więc CustomerAdapter na Adapter i otrzymujemy naszą strukturę wzorca.

Przykład implementacji Adapter Obiektów

Zaczniemy od klasy pomocniczej CustomerFromExternalSystem.

public class CustomerFromExternalSystem
{
   public string Name { get; private set; }
   public string Surname { get; private set; }
   public string City { get; private set; }
   public string Street { get; private set; }
   public string Number { get; private set; }
   public string Email { get; set; }
   public string Phone { get; set; }

   public CustomerFromExternalSystem(string name, string surname, string city, string street, string number, string email, string phone)
   {
      Name = name;
      Surname = surname;
      City = city;
      Street = street;
      Number = number;
      Email = email;
      Phone = phone;
   }
}

Jest to prosta klasa z 7 właściwościami, które ustawiamy w konstruktorze. W ten sposób przekazujemy, że ten obiekt musi zostać utworzony z tymi 7 wartościami pól. Dzięki temu mamy klasę pomocniczą, której potrzebuje nasza adaptacja, więc możemy ją dodać.

Klasa ExternalSystem

/// <summary>
/// Adaptee (adaptowany)
/// </summary>
public class ExternalSystem
{
   public CustomerFromExternalSystem GetCustomer()
   {
      return new CustomerFromExternalSystem("Marcin", "Nowak", "Katowice", "Warszawska", "45/24", "marcin@dev-hobby.pl","345657233");
   }
}

Klasa ExternalSystem udostępnia jedną metodę GetCustomer, która zwraca obiekt CustomerFromExternalSystem.

Klasa Customer

public class Customer
{
   public string FullName { get; private set; }
   public string Address { get; private set; }
   public string Contact { get; set; }

   public Customer(string fullName, string address, string contact)
   {
      FullName = fullName;
      Address = address;
      Contact = contact;
   }
}

Jest to kolejna klasa pomocnicza, której potrzebujemy, i wygląda trochę jak klasa CustomerFromExternalSystem. Ale zamiast Name, Surname ma tylko jedną właściwość FullName, zamiast City, Street, Number ma tylko jedną właściwość Address i zamiast Email, Phone ma Contact. Więc te klasy są podobne, ale różne.

Następną rzeczą, którą robimy, teraz gdy mamy tę klasę pomocniczą, jest utworzenie celu. Używamy do tego interfejsu. Możliwa jest również klasa abstrakcyjna, ale w tym przypadku użyjemy interfejsu, ponieważ w tym przykładzie nie potrzebujemy żadnej podstawowej implementacji.

Interfejs ICustomerAdapter

/// <summary>
/// Target (cel)
/// </summary>
public interface ICustomerAdapter
{
   Customer GetCustomer();
}

Interfejs definiuje metodę GetCustomer, która zwraca Customer zamiast CustomerFromExternalSystem. Innymi słowy, mamy teraz dwa niekompatybilne systemy. Możemy uczynić je kompatybilnymi, implementując ten adapter IcustomerAdapter.

Adapter CustomerAdapter

/// <summary>
/// Adapter
/// </summary>
public class CustomerAdapter : ICustomerAdapter
{
   public ExternalSystem ExternalSystem { get; private set; } = new();

   public Customer GetCustomer()
   {
      // wywołaniemetody z systemu zewnętrznego
      var customerFromExternalSystem = ExternalSystem.GetCustomer();

      // dostosowanie CustomerFromExternalSystem do Customer
      return new Customer($"{customerFromExternalSystem.Name} - {customerFromExternalSystem.Surname}",
                $"{customerFromExternalSystem.City}, {customerFromExternalSystem.Street} {customerFromExternalSystem.Number}",
                $"{customerFromExternalSystem.Email} - {customerFromExternalSystem.Phone}");
   }
}

Kiedy implementujemy interfejs ICustomerAdapter, widzimy, że musimy zaimplementować jedną metodę, GetCustomer. Aby to można zrobić, potrzebujemy również przechowywać właściwość ExternalSystem. Innymi słowy, zamierzamy użyć kompozycji, aby te dwa różne interfejsy lub systemy były kompatybilne. W przypadku demonstracji robimy to ad hoc, ale w razie potrzeby możemy go wstrzyknąć. Teraz pozostaje nam tylko zaimplementować metodę GetCustomer. Robimy to, wywołując system zewnętrzny ExternalSystem, a następnie dostosowując wynik, którym jest CustomerFromExternalSystem do obiektu Customer i to właśnie zwracamy. I to jest nasz wzór Adapter.

Teraz napiszmy przykładowy kod, aby go wypróbować.

using ObjectAdapter;

Console.Title = " Object Adapter";

// Przykład Object Adapter 
ICustomerAdapter adapter = new CustomerAdapter();
var customer = adapter.GetCustomer();

Console.WriteLine($"{customer.FullName} | {customer.Address} | {customer.Contact}");
Console.ReadKey();

Tworzymy nasz adapter, a następnie próbujemy pobrać z niego klienta. Powinno to wywołać system zewnętrzny i zwrócić obiekt Customer. Aby sprawdzić, czy rzeczywiście tak jest, po prostu wypisujemy wynik na konsole.

Wynik działania

Marcin - Nowak | Katowice, Warszawska 45/24 | marcin@dev-hobby.pl - 345657233

Adapter Klasy

Wzorzec adaptera obiektów działa poprzez kompozycję obiektów. Wzorzec adaptera klasy jest odmianą, która opiera się na wielokrotnym dziedziczeniu w celu dostosowania jednego interfejsu do drugiego.

W naszym przypadku nasz CustomerAdapter wywodziłby się z ExternalSystem i Target, zakładając, że oba są abstrakcyjnymi klasami bazowymi. Jest z tym tylko jeden mały problem. C# to język, który preferuje kompozycję nad dziedziczeniem i nie obsługuje dziedziczenia wielokrotnego. Moglibyśmy to zrobić w Javie, ale nie możemy tego zrobić w C#. Możemy to jednak zaimplementować z niewielką różnicą. Możemy pozwolić, aby nasz CustomerAdapter wywodził się z ExternalSystem i implementował interfejs adaptera ICustomerAdapter.

W ten sposób nie potrzebujemy wielokrotnego dziedziczenia. Wdrażamy klasę bazową ExternalSystem i implementujemy interfejs ICustomerAdapter. A ta kombinacja jest obsługiwana przez C#. Przyjrzyjmy się strukturze wzorca.

Struktura

Patrzymy na strukturę obiektu Adapter z niewielką zmianą, w której stwierdzamy, że Adapter implementuje Adaptee, otrzymujemy odmianę wzorca klasy, z którą możemy pracować.

Adapter nie musi już przechowywać obiektu Adaptee w celu wywołania określonego żądania w obiekcie Adaptee. Ponieważ Adapter pochodzi od Adaptee, może teraz wywołać określoną metodę żądania bezpośrednio ze swojej klasy nadrzędnej, Adaptee. Zaimplementujmy to w kodzie.

Przykład implementacji Adapter Klasy

Zaimplementujemy teraz Adapter Klasy. Około 90% kodu jest taka sama. Jest tylko jedna zmiana, którą musimy wprowadzić i to na poziomie klasy CustomerAdapter. Pozostałe klasy zostają niezmienione.

/// <summary>
/// Adapter
/// </summary>
public class CustomerAdapter : ExternalSystem, ICustomerAdapter
{
   public Customer GetCustomer()
   {
      // wywołaniemetody z systemu zewnętrznego
      var customerFromExternalSystem = base.GetCustomer();

      // dostosowanie CustomerFromExternalSystem do Customer
      return new Customer($"{customerFromExternalSystem.Name} - {customerFromExternalSystem.Surname}",
                $"{customerFromExternalSystem.City}, {customerFromExternalSystem.Street} {customerFromExternalSystem.Number}",
                $"{customerFromExternalSystem.Email} - {customerFromExternalSystem.Phone}");
   }
}

Teraz CustomerAdapter dziedziczy z klasy ExternalSystem i implementuje ICustomerAdapter. Oznacza to, że musimy nieco inaczej zaimplementować GetCustomer. Nie przechowujemy już ExternalSystem jako właściwości, więc nie pracujemy już z kompozycją. Zamiast tego wywołujemy GetCustomer w klasie bazowej, klasie ExternalSystem. Możemy to zrobić, ponieważ z niej dziedziczymy.

Wynik działania

Marcin - Nowak | Katowice, Warszawska 45/24 | marcin@dev-hobby.pl - 345657233

Zastosowanie

Adapter jest przydatny, gdy chcemy użyć istniejącej klasy, ale interfejs nie pasuje do tego, którego potrzebujemy, na przykład dostosowując klasę klienta z jednego systemu do drugiego.

Możemy go również użyć, gdy chcemy utworzyć klasę wielokrotnego użytku, adapter, który współpracuje z klasami, które nie mają zgodnych interfejsów.

Możemy go również użyć, gdy musimy użyć kilku istniejących podklas, ale nie chcemy tworzyć dodatkowych podklas dla każdej z nich, ale nadal musimy dostosować ich interfejs. W takim przypadku można użyć wzorca adaptera obiektów, aby dostosować interfejs jego klasy nadrzędnej.

Zalety i wady

Kod klienta działa na wyższym poziomie abstrakcji. Nie musi mieć do czynienia ze szczegółami platformy.

Pojedynczy adapter może pracować z wieloma adapterami, z samym adapterem i wszystkimi jego podklasami, które można na nie rzutować.

Zasada pojedynczej odpowiedzialności. W abstrakcji możesz skupić się na wysokopoziomowej logice, zaś w implementacji na szczegółach platformy.

Zasada otwarte/zamknięte. Możesz wprowadzać nowe abstrakcje i implementacje niezależnie od siebie.

Adapter obiektów utrudnia przesłonięcie zachowania adaptera. Jeśli chcesz to zrobić, musisz utworzyć podklasę adaptera i sprawić, by adapter odwoływał się do podklasy, a nie do samego adaptera.

Inną potencjalną wadą, jak to często bywa, jest dodatkowa złożoność wynikająca z wprowadzenia dodatkowych interfejsów i klas.

Kod źródłowy

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *