Wiosenna WYPRZEDAŻ

Adapter

Znany też jako: Opakowanie, Nakładka, Wrapper

Cel

Adapter jest strukturalnym wzorcem projektowym pozwalającym na współdziałanie ze sobą obiektów o niekompatybilnych interfejsach.

Wzorzec projektowy Adapter

Problem

Wyobraź sobie, że tworzysz aplikację monitorującą giełdę. Pobiera ona dane rynkowe z wielu źródeł w formacie XML, a następnie wyświetla ładnie wyglądające wykresy i diagramy.

Na jakimś etapie postanawiasz wzbogacić aplikację poprzez dodanie inteligentnej biblioteki analitycznej innego producenta. Ale jest haczyk: biblioteka ta działa wyłącznie z danymi w formacie JSON.

Struktura aplikacji przed integracją z biblioteką analityczną

Nie można zastosować biblioteki analitycznej od razu, bo wymaga ona przedstawiania danych w formacie który jest niekompatybilny z twoją aplikacją.

Można przerobić bibliotekę tak, aby obsługiwała XML. To jednak może naruszyć działanie istniejącego kodu korzystającego z tej biblioteki. Co gorsza, możesz nie mieć dostępu do kodu źródłowego biblioteki, co czyni ten plan niewykonalnym.

Rozwiązanie

Możesz stworzyć adapter. Jest to specjalny obiekt konwertujący interfejs jednego z obiektów w taki sposób, że drugi obiekt go rozumie.

Adapter stanowi swego rodzaju opakowanie dla obiektu, ukrywając szczegóły konwersji jakie odbywają się za kulisami. Obiekt opakowywany może nawet nie wiedzieć o istnieniu adaptera. Można na przykład opakować obiekt korzystający z jednostek kilometr i metr w adapter konwertujący te dane na jednostki imperialne, takie jak stopy i mile.

Adaptery mogą nie tylko konwertować dane pomiędzy formatami, ale również pozwolić na współpracę obiektów o różnych interfejsach. Działa to tak:

  1. Adapter uzyskuje interfejs kompatybilny z interfejsem jednego z obiektów.
  2. Za pomocą tego interfejsu, istniejący obiekt może śmiało wywoływać metody adaptera.
  3. Otrzymawszy wywołanie, adapter przekazuje je dalej, ale już w formacie obsługiwanym przez opakowany obiekt.

Czasami jest nawet możliwe stworzenie adaptera dwukierunkowego, potrafiącego konwertować wywołania w obu kierunkach.

Rozwiązanie Adapterem

Wróćmy do naszej aplikacji giełdowej. Aby rozwiązać dylemat niezgodności formatów, można stworzyć adaptery XML-do-JSON dla każdej klasy biblioteki analitycznej, która jest używana bezpośrednio przez nasz kod. Potem zaś możemy dostosować kod tak, aby komunikował się z biblioteką wyłącznie za pomocą adapterów. Gdy adapter otrzyma wywołanie, tłumaczy przychodzące dane XML na strukturę JSON i przekazuje wywołanie dalej, do odpowiedniej metody opakowywanego obiektu analitycznego.

Analogia do prawdziwego życia

Przykład wzorca projektowego Adapter

Walizka przed i po zagranicznej podróży.

Gdy pierwszy raz podróżujesz z Polski do Wielkiej Brytanii, możesz zdziwić się próbując naładować laptop. Standardy wtyczek i gniazd są różne. Twoja wtyczka nie będzie pasować do brytyjskiego gniazdka. Problem można rozwiązać za pomocą przejściówki posiadającej gniazdo typu europejskiego oraz wtyk typu brytyjskiego.

Struktura

Adapter obiektu

Ta implementacja stosuje zasadę kompozycji obiektu: adapter implementuje interfejs jednego z obiektów i opakowuje drugi obiekt. Można to zaimplementować we wszystkich popularnych językach programowania.

Struktura wzorca projektowego Adapter (adapter obiektu)Struktura wzorca projektowego Adapter (adapter obiektu)
  1. Klient jest klasą zawierającą istniejącą logikę biznesową programu.

  2. Interfejs Klienta opisuje protokół którego muszą się trzymać pozostałe klasy by móc współdziałać z kodem klienckim.

  3. Usługa to jakaś użyteczna klasa (na ogół od innego producenta lub starsza). Klient nie jest w stanie użyć jej bezpośrednio z powodu niekompatybilnego interfejsu.

  4. Adapter to klasa która jest w stanie współdziałać zarówno z klientem jak i z usługą: implementuje interfejs klienta, opakowując obiekt-usługę. Adapter otrzymuje wywołania od klienta za pośrednictwem interfejsu klienta i tłumaczy je na wywołania których format zrozumie obiekt udostępniający usługę.

  5. Kod kliencki nie ulegnie sprzęgnięciu z konkretną klasą adaptera, o ile może współpracować z adapterem za pośrednictwem interfejsu klienta. Dzięki temu można wprowadzać nowe typy adapterów do programu bez psucia istniejącego kodu klienckiego. To przydatne, gdy interfejs klasy udostępniającej usługę ulegnie zmianie, bo wówczas wystarczy dodać nową klasę adapter bez konieczności zmiany kodu klienckiego.

Adapter klasy

Ta implementacja stosuje dziedziczenie: adapter dziedziczy interfejsy od obu obiektów jednocześnie. Ten sposób można zaimplementować tylko w językach programowania obsługujących wielokrotne dziedziczenie — np. C++.

Wzorzec projektowy Adapter (adapter klasy)Wzorzec projektowy Adapter (adapter klasy)
  1. Adapter Klasy nie musi opakowywać żadnych obiektów, ponieważ dziedziczy zachowanie zarówno po kliencie, jak i po usłudze. Adaptacja odbywa się wewnątrz przeciążonych metod. Otrzymany w ten sposób adapter może być użyty w miejsce istniejącej klasy klienckiej.

Pseudokod

Ten przykład użycia Adaptera bazuje na klasycznym konflikcie pomiędzy kwadratowym klockiem i okrągłym otworem.

Struktura przykładu wzorca Adapter

Adaptacja kwadratowych klocków do okrągłych otworów.

Adapter udaje, że jest okrągłym klockiem o promieniu równym połowie przekątnej kwadratu (innymi słowy, promienia najmniejszego okręgu w jakim zmieści się kwadratowy klocek).

// Powiedzmy że masz dwie klasy o kompatybilnych interfejsach:
// RoundHole i RoundPeg.
class RoundHole is
    constructor RoundHole(radius) { ... }

    method getRadius() is
        // Zwraca promień otworu.

    method fits(peg: RoundPeg) is
        return this.getRadius() >= peg.getRadius()

class RoundPeg is
    constructor RoundPeg(radius) { ... }

    method getRadius() is
        // Zwraca promień klocka (ang. peg).


// Mamy jednak niekompatybilną klasę: SquarePeg.
class SquarePeg is
    constructor SquarePeg(width) { ... }

    method getWidth() is
        // Zwróć długość boku kwadratowego klocka.

// Klasa adapter pozwala zmieścić kwadratowy klocek w okrągłym
// otworze. Rozszerza klasę RoundPeg pozwalając obiektom-
// adapterom zachowywać się jak okrągłe klocki.
class SquarePegAdapter extends RoundPeg is
    // W rzeczywistości, adapter zawiera instancję klasy
    // SquarePeg.
    private field peg: SquarePeg

    constructor SquarePegAdapter(peg: SquarePeg) is
        this.peg = peg

    method getRadius() is
        // Ten adapter udaje okrągły klocek o promieniu
        // pozwalającym zmieścić w sobie opakowywany kwadratowy
        // klocek.
        return peg.getWidth() * Math.sqrt(2) / 2


// Gdzieś w kodzie klienta.
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // prawda

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // to się nie skompiluje (niezgodne typy)

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // prawda
hole.fits(large_sqpeg_adapter) // fałsz

Zastosowanie

Stosuj klasę Adapter gdy chcesz wykorzystać jakąś istniejącą klasę, ale jej interfejs nie jest kompatybilny z resztą twojego programu.

Wzorzec Adapter pozwala utworzyć klasę która stanowi warstwę pośredniczącą pomiędzy twoim kodem, a klasą pochodzącą z zewnątrz, lub inną, posiadającą jakiś nietypowy interfejs.

Stosuj ten wzorzec gdy chcesz wykorzystać ponownie wiele istniejących podklas którym brakuje jakiejś wspólnej funkcjonalności, niedającej się dodać do ich nadklasy.

Możesz rozszerzyć każdą podklasę i umieścić potrzebną funkcjonalność w nowych klasach pochodnych. Jednak wtedy trzeba by było duplikować kod i umieścić go we wszystkich nowych klasach, a to psuje zapach kodu.

Znacznie bardziej eleganckim rozwiązaniem jest umieszczenie brakującej funkcjonalności w klasie adapter i opakowanie nią obiektów pozbawionych potrzebnych funkcji. Aby to zadziałało, klasy docelowe muszą mieć wspólny interfejs, a pole adaptera musi być z nim zgodne. Podejście to bardzo przypomina wzorzec Dekorator.

Jak zaimplementować

  1. Upewnij się, że masz przynajmniej dwie klasy o niekompatybilnych interfejsach:

    • Jakąś użyteczną klasę usługową której nie możesz zmienić (innego producenta, przestarzałą, albo ze zbyt wieloma istniejącymi zależnościami)
    • Jedną lub wiele klas klienckich które zyskałyby na możliwości skorzystania z powyższej usługi.
  2. Zadeklaruj interfejs klienta i opisz jak ma wyglądać komunikacja klientów z usługą.

  3. Stwórz klasę adapter zgodną z interfejsem klienckim. Póki co, pozostaw metody pustymi.

  4. Dodaj pole do klasy adapter, które przechowa odniesienie do obiektu usługi. Typową praktyką jest inicjalizacja tego pola za pośrednictwem konstruktora, ale czasem wygodniej jest przekazać usługę adapterowi wywołując jego metody.

  5. Jeden po drugim, zaimplementuj wszystkie metody interfejsu klienta w klasie adapter. Adapter powinien delegować większość faktycznej pracy obiektowi oferującemu usługę i zajmować się wyłącznie pośrednictwem lub konwersją danych.

  6. Klienci powinni stosować adapter za pośrednictwem interfejsu klienta. Pozwoli to zmieniać lub rozszerzać adaptery bez wpływania na kodu kliencki.

Zalety i wady

  • Zasada pojedynczej odpowiedzialności. Można oddzielić interfejs lub kod konwertujący dane od głównej logiki biznesowej programu.
  • Zasada otwarte/zamknięte. Można wprowadzać do programu nowe typy adapterów bez psucia istniejącego kodu klienckiego, o ile będzie on korzystał z adapterów poprzez interfejs kliencki.
  • Ogólna złożoność kodu zwiększa się, ponieważ trzeba wprowadzić zestaw nowych interfejsów i klas. Czasem łatwiej zmienić klasę udostępniającą jakąś potrzebną usługę, aby pasowała do reszty kodu.

Powiązania z innymi wzorcami

  • Most zazwyczaj wykorzystuje się od początku projektu, by pozwolić na niezależną pracę nad poszczególnymi częściami aplikacji. Z drugiej strony, Adapter jest rozwiązaniem stosowanym w istniejącej aplikacji w celu umożliwienia współpracy pomiędzy niekompatybilnymi klasami.

  • Adapter zapewnia zupełnie inny interfejs dostępu do istniejącego obiektu. Z drugiej strony, w przypadku wzorca Dekorator interfejs albo pozostaje taki sam, albo zostaje rozszerzony. Ponadto, Decorator obsługuje rekurencyjną kompozycję, co nie jest możliwe w przypadku użycia Adapter.

  • Za pomocą Adapter można uzyskać dostęp do istniejącego obiektu za pośrednictwem innego interfejsu. W przypadku Pełnomocnik interfejs pozostaje taki sam. Za pomocą Dekorator uzyskuje się dostęp do obiektu za pośrednictwem ulepszonego interfejsu.

  • Fasada definiuje nowy interfejs istniejącym obiektom, zaś Adapter zakłada zwiększenie użyteczności zastanego interfejsu. Adapter na ogół opakowuje pojedynczy obiekt, zaś Fasada obejmuje cały podsystem obiektów.

  • Most, Stan, Strategia (i w pewnym stopniu Adapter) mają podobną strukturę. Wszystkie oparte są na kompozycji, co oznacza delegowanie zadań innym obiektom. Jednak każdy z tych wzorców rozwiązuje inne problemy. Wzorzec nie jest bowiem tylko receptą na ustrukturyzowanie kodu w pewien sposób, lecz także informacją dla innych deweloperów o charakterze rozwiązywanego problemu.

Przykłady kodu

Adapter w języku C# Adapter w języku C++ Adapter w języku Go Adapter w języku Java Adapter w języku PHP Adapter w języku Python Adapter w języku Ruby Adapter w języku Rust Adapter w języku Swift Adapter w języku TypeScript