Adapter

Wzorce projektowe

  |   6 min czytania

Zastosowanie

Adapter (ang. Adapter) (wzorzec strukturalny) umożliwia współpracę dwóch klas o niekompatybilnych interfejsach dzięki przekształceniu interfejsu jednej klasy na zrozumiałą postać dla drugiej. Adapter zwany inaczej Wrapper jest swego rodzaju przejściówką między dwoma bytami. Wyróżnia się dwa warianty klasycznej postaci tego wzorca Adapter Klasowy oraz Adapter Obiektowy. Wybór rodzaju Adaptera w zależności od warunków może być dowolny lub podyktowany ograniczeniami. W sytuacji gdy nie można rozszerzyć klasy adaptowanej używany jest Adapter Obiektowy, natomiast gdy klasa adaptowana jest abstrakcyjna stosuje się Adapter Klasowy. Ze względu zagrożenia jakie niesie ze sobą używanie Adaptera Klasowego w większości sytuacji stosuje się Adapter Obiektowy. Ponadto mówi się o Adapterze Dwukierunkowym, w którym translacja zachodzi w obie strony, tzn. klient jest jest także klasą adapatowaną. Taki mechanizm może posłużyć np. do konwersji typów.

Ograniczenia

Przy tworzeniu Adaptera należy mieć na uwadzę, że wraz ze wzrostem niekompatybilności maleje wydajność co może stanowić problem w niektórych aplikacjach. Adapter Obiektowy nie ma możliwości przeładowania metod klasy adaptowanej, natomiast Adapter Klasowy nie pozwala na adaptowanie podklas klasy adaptowanej. Adapter Klasowy ściśle wiąże klienta z klasą adaptowaną co w przypadku rozrostu systemu czy zmiany wymagań zmniejsza reużywalność kodu.

Użycie

Stosowany jest głównie tam, gdzie wykorzystanie istniejącej klasy w celu komunikacji z inną klasą jest niemożliwe ze względu na niekompatybilność interfejsu. Ponadto Adapter używany jest w sytuacjach, gdy tworzona klasa będzie współpracować z klasami o nieokreślonych interfejsach.

Implementacja

Adapter Klasowy dziedziczy po klasie adaptowanej Adaptee oraz implementuje klase klienta Client. Adapter Obiektowy implementuje klase klienta Client oraz zawiera instancję klasy adaptowanej Adaptee.

Adapter diagram

Klasa Client oraz Adaptee są niekompatybilne co przedstawia poniższy listing.

public interface Client {
  
    void operation1();
    void operation2();
}

public class Adaptee {

    public void method1() {
        //some work
    }

    public void method2() {
        //some work 
    }

    public void method3() {
        //some work 
    }
}

Aby możliwa była komunikacja należy między nimi wprowadzić Adapter. Poniższy listing przedstawia implementacje wzorca w dwóch wariantach.

public class AdapterClass extends Adaptee implements Client {

    @Override
    public void operation1() {
        method1();
    }

    @Override
    public void operation2() {
        method2();
    }
}

public class AdapterObject implements Client {
  
    private Adaptee adaptee;

    public Client() {
        this.adaptee = new Adaptee();
        //some parameters can be passed to initialize Adaptee
    }

    @Override
    public void operation1() {
        adaptee.method1();
    }

    @Override
    public void operation2() {
        adaptee.method2();
    }
}

Tak przygotowane adaptery umożliwią wywołanie metod klasy adaptowanej Adaptee z poziomu klasy Client co przedstawia poniższy listing.

Client clientClass = new AdapterClass();
clientClass.operation1(); //runs method1() from Adaptee

Client clientObject = new AdapterObject();
clientObject.operation2(); //runs method2() from Adaptee

Przykład

Komunikator Messenger umożliwia wysyłanie wiadomości tekstowych oraz plików między rozmówcami wykorzystując w tym celu wewnętrzną biblioteke protokołu sieciowego. Ponadto aplikacja pozwala na zmianę statusu. Poniższy listing przedstawia implementacje komunikatora Messenger wraz z jego protokołem MessageProtocol.

public class Messenger {

    private IMessageProtocol protocol;

    public Messenger(IMessageProtocol protocol) {
        this.protocol = protocol;
    }

    public void sendMessage(String text) {
        protocol.sendText(text);
    }

    public void sendMessage(File file) {
        protocol.sendFile(file);
    }

    public void changeStatus(String text) {
        //set and show status
    }
}

public class MessageProtocol implements IMessageProtocol {

    @Override
    public void sendText(String text) {
        //converting and sending text
    }

    @Override
    public void sendFile(File file) {
        //converting and sending file
    }
}

public interface IMessageProtocol {

    void sendText(String text);
    void sendFile(File file);
}

W pewnym momencie cyklu życia projektu zachodzi potrzeba wymiany protokołu sieciowego MessageProtocol na bardziej wydajną biblioteke zewnętrzną ExternalMessageProtocol. Dodatkowo każda operacja ma wysyłać dane do analityki. Ze względu na rozmiar projektu nie wchodzi w grę zmiana typu instancji protokołu oraz wywołań metod we wszystkich wystąpieniach obiektu klasy Messenger. Rozwiązaniem tego problemu jest zastosowanie Adaptera, którego implementacja przedstawiona jest poniżej.

public class MessageProtocolAdapter implements IMessageProtocol {

    private ExternalMessageProtocol protocol = new ExternalMessageProtocol();

    public MessageProtocolAdapter() {
        this.protocol = new ExternalMessageProtocol();
    }

    @Override
    public void sendText(String text) {
        Analytics.report("Message");
        protocol.sendString(text);
    }

    @Override
    public void sendFile(File file) {
        Analytics.report("File");
        protocol.sendBytes(protocol.getBytes(file));
    }
}

Aby skorzystać z nowego rozwiązania wystarczy przekazać referencje Adaptera do aplikacji w następujący sposób.

Messenger newMessenger = new Messenger(new MessageProtocolAdapter());
newMessenger.sendMessage("Message from external library"); //send optimized message and analytics

Messenger oldMessenger = new Messenger(new MessageProtocol());
oldMessenger.sendMessage("Message from internal library"); //send only message

Biblioteki

Adapter jest standardowym elementem biblioteki Android. Jego celem jest dostarczenie danych do widoku kolekcji AdaperView, a także stworzenie widoków View dla każdego elementu ze zbioru danych. Metody asList klasy Arrays oraz list klasy Collections są przykładem implementacji wzorca w standardowym pakiecie Java.