CodeGym /Blog Java /Random-PL /Jakie problemy rozwiązuje wzorzec projektowy adaptera?
Autor
Artem Divertitto
Senior Android Developer at United Tech

Jakie problemy rozwiązuje wzorzec projektowy adaptera?

Opublikowano w grupie Random-PL
Rozwój oprogramowania jest utrudniony przez niekompatybilne komponenty, które muszą ze sobą współpracować. Na przykład, jeśli potrzebujesz zintegrować nową bibliotekę ze starą platformą napisaną we wcześniejszych wersjach Javy, możesz napotkać niekompatybilne obiekty, a raczej niekompatybilne interfejsy. Jakie problemy rozwiązuje wzorzec projektowy adaptera?  - 1Co zrobić w tym przypadku? Przepisać kod? Nie możemy tego zrobić, ponieważ analiza systemu zajmie dużo czasu lub zostanie naruszona wewnętrzna logika aplikacji. Aby rozwiązać ten problem, utworzono wzorzec adaptera. Pomaga obiektom z niekompatybilnymi interfejsami współpracować. Zobaczmy, jak go używać!

Więcej o problemie

Najpierw zasymulujemy zachowanie starego systemu. Załóżmy, że generuje wymówki za spóźnienie się do pracy lub szkoły. Aby to zrobić, ma interfejs , Excusektóry ma metody i . generateExcuse()likeExcuse()dislikeExcuse()

public interface Excuse {
   String generateExcuse();
   void likeExcuse(String excuse);
   void dislikeExcuse(String excuse);
}
Klasa WorkExcuseimplementuje ten interfejs:

public class WorkExcuse implements Excuse {
   private String[] excuses = {"in an incredible confluence of circumstances, I ran out of hot water and had to wait until sunlight, focused using a magnifying glass, heated a mug of water so that I could wash.",
   "the artificial intelligence in my alarm clock failed me, waking me up an hour earlier than normal. Because it is winter, I thought it was still nighttime and I fell back asleep. Everything after that is a bit hazy.",
   "my pre-holiday mood slows metabolic processes in my body, leading to depression and insomnia."};
   private String [] apologies = {"This will not happen again, of course. I'm very sorry.", "I apologize for my unprofessional behavior.", "There is no excuse for my actions. I am not worthy of this position."};

   @Override
   public String generateExcuse() { // Randomly select an excuse from the array
       String result = "I was late today because " + excuses[(int) Math.round(Math.random() + 1)] + "\\n" +
               apologies[(int) Math.round(Math.random() + 1)];
       return result;
   }

   @Override
   public void likeExcuse(String excuse) {
       // Duplicate the element in the array so that its chances of being chosen are higher
   }

   @Override
   public void dislikeExcuse(String excuse) {
       // Remove the item from the array
   }
}
Przetestujmy nasz przykład:

Excuse excuse = new WorkExcuse();
System.out.println(excuse.generateExcuse());
Wyjście:

"I was late today because my pre-holiday mood slows metabolic processes in my body, leading to depression and insomnia.
I apologize for my unprofessional behavior.
Teraz wyobraź sobie, że uruchomiłeś usługę generującą wymówki, zebrałeś statystyki i zauważyłeś, że większość Twoich użytkowników to studenci. Aby lepiej służyć tej grupie, poprosiłeś innego programistę o stworzenie systemu, który generuje wymówki specjalnie dla studentów. Zespół programistów przeprowadził badania rynku, uszeregował wymówki, podłączył sztuczną inteligencję i zintegrował usługę z raportami o ruchu drogowym, prognozami pogody i tak dalej. Teraz masz bibliotekę do generowania wymówek dla studentów, ale ma ona inny interfejs: StudentExcuse.

public interface StudentExcuse {
   String generateExcuse();
   void dislikeExcuse(String excuse);
}
Ten interfejs ma dwie metody: generateExcuse, która generuje wymówkę, oraz dislikeExcuse, która zapobiega ponownemu pojawieniu się wymówki w przyszłości. Biblioteki strony trzeciej nie można edytować, tzn. nie można zmienić jej kodu źródłowego. Mamy teraz system z dwiema klasami, które implementują interfejs Excuse, oraz bibliotekę z SuperStudentExcuseklasą, która implementuje StudentExcuseinterfejs:

public class SuperStudentExcuse implements StudentExcuse {
   @Override
   public String generateExcuse() {
       // Logic for the new functionality
       return "An incredible excuse adapted to the current weather conditions, traffic jams, or delays in public transport schedules.";
   }

   @Override
   public void dislikeExcuse(String excuse) {
       // Adds the reason to a blacklist
   }
}
Kodu nie można zmienić. Obecna hierarchia klas wygląda następująco: Jakie problemy rozwiązuje wzorzec projektowy adaptera?  - 2Ta wersja systemu działa tylko z interfejsem Excuse. Nie możesz przepisać kodu: w dużej aplikacji wprowadzanie takich zmian może stać się długotrwałym procesem lub zepsuć logikę aplikacji. Moglibyśmy wprowadzić podstawowy interfejs i rozszerzyć hierarchię: Jakie problemy rozwiązuje wzorzec projektowy adaptera?  - 3Aby to zrobić, musimy zmienić nazwę Excuseinterfejsu. Ale dodatkowa hierarchia jest niepożądana w poważnych aplikacjach: wprowadzenie wspólnego elementu głównego psuje architekturę. Należy zaimplementować klasę pośrednią, która pozwoli nam korzystać zarówno z nowej, jak i starej funkcjonalności przy minimalnych stratach. Krótko mówiąc, potrzebujesz adaptera .

Zasada wzorca adaptera

Adapter jest obiektem pośrednim, który umożliwia zrozumienie wywołań metod jednego obiektu przez inny. Zaimplementujmy adapter dla naszego przykładu i nazwijmy go Middleware. Nasz adapter musi zaimplementować interfejs zgodny z jednym z obiektów. Niech będzie Excuse. Pozwala Middlewareto wywołać metody pierwszego obiektu. Middlewareodbiera połączenia i przekazuje je w sposób kompatybilny do drugiego obiektu. Oto implementacja Middlewarez metodami generateExcusei dislikeExcuse:

public class Middleware implements Excuse { // 1. Middleware becomes compatible with WorkExcuse objects via the Excuse interface

   private StudentExcuse superStudentExcuse;

   public Middleware(StudentExcuse excuse) { // 2. Get a reference to the object being adapted
       this.superStudentExcuse = excuse;
   }

   @Override
   public String generateExcuse() {
       return superStudentExcuse.generateExcuse(); // 3. The adapter implements an interface method
   }

    @Override
    public void dislikeExcuse(String excuse) {
        // The method first adds the excuse to the blacklist,
        // Then passes it to the dislikeExcuse method of the superStudentExcuse object.
    }
   // The likeExcuse method will appear later
}
Testowanie (w kodzie klienta):

public class Test {
   public static void main(String[] args) {
       Excuse excuse = new WorkExcuse(); // We create objects of the classes
       StudentExcuse newExcuse = new SuperStudentExcuse(); // that must be compatible.
       System.out.println("An ordinary excuse for an employee:");
       System.out.println(excuse.generateExcuse());
       System.out.println("\n");
       Excuse adaptedStudentExcuse = new Middleware(newExcuse); // Wrap the new functionality in the adapter object
       System.out.println("Using new functionality with the adapter:");
       System.out.println(adaptedStudentExcuse.generateExcuse()); // The adapter calls the adapted method
   }
}
Wyjście:

An ordinary excuse for an employee:
I was late today because my pre-holiday mood slows metabolic processes in my body, leading to depression and insomnia.
There is no excuse for my actions. I am not worthy of this position. Using new functionality with the adapter:
Niesamowita wymówka dostosowana do aktualnych warunków pogodowych, korków czy opóźnień w kursowaniu komunikacji miejskiej. Metoda generateExcusepo prostu przekazuje wywołanie do innego obiektu, bez żadnych dodatkowych zmian. Metoda dislikeExcusewymagała od nas najpierw umieszczenia wymówki na czarnej liście. Możliwość wykonywania pośredniego przetwarzania danych jest powodem, dla którego ludzie uwielbiają wzorzec adaptera. Ale co z likeExcusemetodą, która jest częścią interfejsu Excuse, ale nie jest częścią interfejsu StudentExcuse? Nowa funkcjonalność nie obsługuje tej operacji. Został UnsupportedOperationExceptionwymyślony dla tej sytuacji. Jest generowany, jeśli żądana operacja nie jest obsługiwana. Użyjmy tego. Tak Middlewarewygląda nowa implementacja klasy:

public class Middleware implements Excuse {

   private StudentExcuse superStudentExcuse;

   public Middleware(StudentExcuse excuse) {
       this.superStudentExcuse = excuse;
   }

   @Override
   public String generateExcuse() {
       return superStudentExcuse.generateExcuse();
   }

   @Override
   public void likeExcuse(String excuse) {
       throw new UnsupportedOperationException("The likeExcuse method is not supported by the new functionality");
   }

   @Override
   public void dislikeExcuse(String excuse) {
       // The method accesses a database to fetch additional information,
       // and then passes it to the superStudentExcuse object's dislikeExcuse method.
   }
}
Na pierwszy rzut oka to rozwiązanie nie wydaje się zbyt dobre, ale naśladowanie funkcjonalności może skomplikować sytuację. Jeśli klient zwraca na to uwagę, a adapter jest dobrze udokumentowany, takie rozwiązanie jest do zaakceptowania.

Kiedy używać adaptera

  1. Kiedy musisz użyć klasy innej firmy, ale jej interfejs jest niekompatybilny z główną aplikacją. Powyższy przykład pokazuje, jak utworzyć obiekt adaptera, który zawija wywołania w formacie zrozumiałym dla obiektu docelowego.

  2. Gdy kilka istniejących podklas wymaga wspólnej funkcjonalności. Zamiast tworzyć dodatkowe podklasy (co doprowadzi do powielania kodu), lepiej jest użyć adaptera.

Zalety i wady

Zaleta: Adapter ukrywa przed klientem szczegóły przetwarzania żądań z jednego obiektu do drugiego. Kod klienta nie myśli o formatowaniu danych ani obsłudze wywołań metody docelowej. Jest to zbyt skomplikowane, a programiści są leniwi :) Wada: Baza kodu projektu jest skomplikowana przez dodatkowe klasy. Jeśli masz wiele niekompatybilnych interfejsów, liczba dodatkowych klas może stać się nie do opanowania.

Nie myl adaptera z fasadą lub dekoratorem

Wystarczy powierzchowna inspekcja, aby adapter mógł zostać pomylony z wzorami elewacji i dekoratorów. Różnica między adapterem a fasadą polega na tym, że fasada wprowadza nowy interfejs i obejmuje cały podsystem. A dekorator, w przeciwieństwie do adaptera, zmienia sam obiekt, a nie interfejs.

Algorytm krok po kroku

  1. Najpierw upewnij się, że masz problem, który ten wzorzec może rozwiązać.

  2. Zdefiniuj interfejs klienta, który będzie używany do pośredniej interakcji z niekompatybilnymi obiektami.

  3. Spraw, aby klasa adaptera odziedziczyła interfejs zdefiniowany w poprzednim kroku.

  4. W klasie adaptera utwórz pole do przechowywania odwołania do obiektu adaptera. To odwołanie jest przekazywane do konstruktora.

  5. Zaimplementuj wszystkie metody interfejsu klienta w adapterze. Metoda może:

    • Przekazuj połączenia bez wprowadzania żadnych zmian

    • Modyfikować lub uzupełniać dane, zwiększać/zmniejszać liczbę wywołań metody docelowej itp.

    • W skrajnych przypadkach, jeśli dana metoda pozostaje niekompatybilna, wyrzuć wyjątek UnsupportedOperationException. Nieobsługiwane operacje muszą być ściśle udokumentowane.

  6. Jeśli aplikacja korzysta z klasy adaptera tylko poprzez interfejs klienta (jak w powyższym przykładzie), to adapter można w przyszłości bezproblemowo rozszerzyć.

Oczywiście ten wzorzec projektowy nie jest panaceum na wszystkie bolączki, ale może pomóc w eleganckim rozwiązaniu problemu niekompatybilności między obiektami o różnych interfejsach. Deweloper, który zna podstawowe wzorce, jest o kilka kroków przed tymi, którzy wiedzą tylko, jak pisać algorytmy, ponieważ wzorce projektowe są wymagane do tworzenia poważnych aplikacji. Ponowne użycie kodu nie jest takie trudne, a konserwacja staje się przyjemnością. To wszystko na dzisiaj! Ale wkrótce będziemy dalej poznawać różne wzorce projektowe :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION