Czytasz jeden z artykułów opisujących wzorce projektowe. Jeśli interesuje Cię ten temat zapraszam Cię do lektury pozostałych artykułów, które powstały w ramach tej serii – wzorce projektowe

Problem do rozwiązania

Pewnie wiesz, że w różnych krajach gniazdka mogą wyglądać inaczej niż to, co możesz zobaczyć na co dzień. Charakterystyka prądu w takim gniazdku także może być różna. Załóżmy, że jedziesz do Wielkiej Brytanii, albo do Stanów Zjednoczonych. Zabierasz ze sobą laptopa i ładowarkę. Bateria wystarcza Ci na czas lotu. Po przylocie na miejsce chcesz uzupełnić baterię w pierwszym wolnym gniazdku na lotnisku.

Tu pojawia się problem. Wtyczka z Twojej ładowarki nie pasuje do gniazdka. Można powiedzieć, że gniazdko i wtyczka nie są ze sobą kompatybilne. Przypominasz sobie jednak, że przezornie udało Ci się zapakować przejściówkę. Przejściówka sprawi, że możesz podłączyć swoją ładowarkę do gniazdka.

Problem tego typu może także występować w projektach informatycznych. Przejściówka, która pozwala włączyć wtyczkę do innego gniazdka to nic innego jak adapter.

Problemem do rozwiązania jest zatem użycie obiektu, w miejscu gdzie jego interfejs nie jest obsługiwany. Adapter rozwiązuje ten problem “tłumacząc” go na coś zrozumiałego dla klienta.

Błyskawiczny kurs UML

Zanim przejdę do omówienia diagramów, które pokazują powiązania klas i interfejsów w tym wzorcu projektowym musisz dowiedzieć się czegoś o UML’u.

UML (ang. Unified Modeling Language) składa się z kilkunastu rodzajów diagramów. Jest to zestaw, który pozwala na wizualną reprezentację projektu informatycznego. W ramach serii opisującej wzorce projektowe będę korzystał z zupełnych podstaw tej notacji. Będę używał głównie diagramów klas. Chociaż nie jestem wielkim fanem UML’a, to taki sposób prezentacji w tym przypadku wydaje mi się najlepszy.

Do zrozumienia diagramów z tego artykuły wystarczy Ci ten przykład:

Przykładowy diagram UML.

Na tym diagramie możesz zobaczyć:

  • trzy klasy – prostokąty z napisami User, LinkedList, Object,
  • dwa interfejsy – prostokąty oznaczone adnotacją <<interfejs>> z napisami List, Collection,
  • dziedziczenie – strzałka z ciągłą linią i z pustym grotem, na przykład pomiędzy LinkedList a Object czy List a Collection,
  • implementację interfejsu – strzałka z przerywaną linią i z pustym grotem pomiędzy LinkedList a List,
  • zależność – strzałkę z ciągłą linią pomiędzy User a LinkedList.

Kod w języku Java zgodny z tym diagramem może wyglądać tak (część diagramu dotycząca elementów biblioteki standardowej nie jest tu widoczna):

public class User extends Object {
    private LinkedList<String> notes;
}

Te podstawy w zupełności wystarczą Ci do zrozumienia poniższych przykładów.

Wzorzec adapter

Diagramy klas

Istnieją dwa sposoby implementacji adaptera. Jeden z nich używa kompozycji, drugi dziedziczenia. Diagramy poniżej pokazują tę subtelną różnicę:

Wzorzec adapter zaimplementowany przy pomocy kompozycji.
Wzorzec adapter zaimplementowany przy pomocy dziedziczenia.

W obu przypadkach klasa DoAdaptacji nie implementuje bezpośrednio interfejsu Zależność. Ten interfejs implementuje klasa Adapter. Także w obu przypadkach Klient reprezentuje klasę, która używa interfejsu Zależność. Zatem użycie klasy Adapter pozwala na pośrednie użycie klasy DoAdaptacji przez klasę Klient.

Zaletą stosowania tego wzorca projektowego jest to, że klasa DoAdaptacji nie musi być modyfikowana, aby spełnić interfejs wymagany przez klasę Klient. Czasami nawet taka modyfikacja nie jest możliwa.

Pobierz opracowania zadań z rozmów kwalifikacyjnych

Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 2501 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.

Przykładowa implementacja adaptera

Wyobraź sobie sytuację, w której mamy macierz kwadratową. Macierz reprezentowana jest przez obiekt implementujący interfejs Matrix:

public interface Matrix {
    int get(int x, int y);
    int size();
}

Dodatkowo istnieje klasa MatrixOperations, która definiuje zestaw metod operujących na takich macierzach. Przykład poniżej pokazuje metodę largest, która zwraca największy element z macierzy:

public class MatrixOperations {
    public static int largest(Matrix m) {
        if (m.size() == 0) {
            throw new IllegalArgumentException("Matrix is empty!");
        }
        int largest = m.get(0, 0);
        for (int x = 0; x < m.size(); x++) {
            for (int y = 0; y < m.size(); y++) {
                if (m.get(x, y) > largest) {
                    largest = m.get(x, y);
                }
            }
        }
        return largest;
    }
}

Przekładając to na diagramy, które pokazałem wyżej to:

  • KlientMatrixOperations,
  • ZależnośćMatrix.

Adapter przy użyciu kompozycji

Standardowo macierz można reprezentować przez tablicę dwuwymiarową. ArrayMatrix to adapter, który wykorzystuje kompozycję. W tym przypadku opakowuje on tablicę dwuwymiarową – int[][], udostępniając interfejs Matrix:

public class ArrayMatrix implements Matrix {
    private final int[][] matrix;

    public ArrayMatrix(int[][] matrix) {
        this.matrix = matrix;
    }

    @Override
    public int get(int x, int y) {
        return matrix[y][x];
    }

    @Override
    public int size() {
        return matrix.length;
    }
}

W tym przypadku:

  • AdapterArrayMatrix,
  • DoAdaptacjiint[][].

Wszystko ładnie działa. Do czasu. Pojawiło się wymaganie, które zakłada, że musisz przechować bardzo dużą i rzadką macierz. Rzadka macierz to taka, w której większość elementów ma wartość 0. Jest to problem, ponieważ ArrayMatrix wymaga ciągłych obszarów pamięci. Dodatkowo marnuje ją przechowuje wartości 0, które można pominąć.

Z pomocą przychodzi inna implementacja adaptera.

Adapter przy użyciu dziedziczenia

Tym razem adapter wykorzystuje dziedziczenie:

public class MapMatrix extends HashMap<String, Integer> implements Matrix {
    private final int size;

    public MapMatrix(int size) {
        this.size = size;
    }

    @Override
    public int get(int x, int y) {
        assertBoundaries(x, y);
        return this.getOrDefault(key(x, y), 0);
    }
    
    public void set(int x, int y, int value) {
        assertBoundaries(x, y);
        put(key(x, y), value);
    }

    @Override
    public int size() {
        return size;
    }

    private String key(int x, int y) {
        return x + "," + y;
    }

    private void assertBoundaries(int x, int y) {
        if (x < 0 || x > size || y < 0 || y > size) {
            throw new IllegalArgumentException(key(x, y));
        }
    }
}

W tym przypadku:

  • AdapterMapMatrix,
  • DoAdaptacjiHashMap.

Ćwiczenie do wykonania

Ćwiczenie polega na zaimplementowaniu adaptera. Przerób adapter MapMatrix w taki sposób, aby wykorzystywał kompozycję.

Dodatkowe materiały do nauki

Bez wątpienia klasyką tematu jest książka Design Patterns – Gamma, Helm, Johnson, Vlissides. Jeśli miałbym polecić wyłącznie jedno źródło to poprzestałbym na tej książce.

Zachęcam Cię też do zajrzenia do kodu źródłowego, który użyłem w tym artykule.

Podsumowanie

Po przeczytaniu tego artykułu wiesz czym jest wzorzec projektowy adapter. Znasz przykłady zastosowania tego wzorca. Rozwiązując ćwiczenie udało Ci się zastosować tę wiedzę w praktyce.

Mam nadzieje, że artykuł przypadł Ci do gustu. Na koniec mam do Ciebie prośbę. Jeśli ktoś z Twoich znajomych mógłby skorzystać z tego artykułu proszę przekaż mu linka. Dzięki temu pomożesz mi dotrzeć do nowych Czytelników. Z góry dziękuję!

Jeśli nie chcesz pominąć nowych artykułów polub Samouczka na Facebook’u i zapisz się do samouczkowego newslettera. Trzymaj się!

Pobierz opracowania zadań z rozmów kwalifikacyjnych

Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 2501 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.

Zostaw komentarz