Więcej szczegółów problemu
Najpierw zasymulujmy zachowanie starego systemu. Załóżmy, że generuje powody spóźnienia się do pracy lub szkoły. Aby to zrobić, mamy interfejsExcuse
zawierający metody generateExcuse()
i likeExcuse()
.dislikeExcuse()
public interface Excuse {
String generateExcuse();
void likeExcuse(String excuse);
void dislikeExcuse(String excuse);
}
Interfejs ten jest implementowany przez klasę WorkExcuse
:
public class WorkExcuse implements Excuse {
private String[] reasonOptions = {"по невероятному стечению обстоятельств у нас в доме закончилась горячая вода и я ждал, пока солнечный свет, сконцентрированный через лупу, нагреет кружку воды, чтобы я мог умыться.",
"искусственный интеллект в моем будильнике подвел меня и разбудил на час раньше обычного. Поскольку сейчас зима, я думал что еще ночь и уснул. Дальше все Jak в тумане.",
"предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице."};
private String[] sorryOptions = {"Это, конечно, не повторится, мне очень жаль.", "Прошу меня извинить за непрофессиональное поведение.", "Нет оправдания моему поступку. Я недостоин этой должности."};
@Override
public String generateExcuse() { // Случайно выбираем отговорку из массива
String result = "Я сегодня опоздал, потому что " + reasonOptions[(int) Math.round(Math.random() + 1)] + "\n" +
sorryOptions[(int) Math.round(Math.random() + 1)];
return result;
}
@Override
public void likeExcuse(String excuse) {
// Дублируем элемент в массиве, чтобы шанс его выпадения был выше
}
@Override
public void dislikeExcuse(String excuse) {
// Удаляем элемент из массива
}
}
Przetestujmy przykład:
Excuse excuse = new WorkExcuse();
System.out.println(excuse.generateExcuse());
Wniosek:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице.
Прошу меня извинить за непрофессиональное поведение.
A teraz wyobraźmy sobie, że uruchomiłeś usługę, zebrałeś statystyki i zauważyłeś, że większość użytkowników serwisu to studenci. Aby ulepszyć go na potrzeby tej grupy, specjalnie dla niej zamówiłeś u innego dewelopera system generowania wymówek. Zespół programistów przeprowadził badania, opracował oceny, połączył sztuczną inteligencję, dodał integrację z korkami, pogodą i tak dalej. Teraz masz bibliotekę do generowania wymówek dla uczniów, ale interfejs interakcji z nią jest inny - StudentExcuse
:
public interface StudentExcuse {
String generateExcuse();
void dislikeExcuse(String excuse);
}
Interfejs posiada dwie metody: generateExcuse
, która generuje wymówkę oraz dislikeExcuse
, która blokuje wymówkę, aby nie pojawiała się ona w przyszłości. Biblioteka innej firmy jest zamknięta do edycji - nie można zmienić jej kodu źródłowego. W rezultacie w Twoim systemie znajdują się dwie klasy implementujące interfejs Excuse
oraz biblioteka z klasą SuperStudentExcuse
implementującą interfejs StudentExcuse
:
public class SuperStudentExcuse implements StudentExcuse {
@Override
public String generateExcuse() {
// Логика нового функционала
return "Невероятная отговорка, адаптированная под текущее состояние погоды, пробки Lub сбои в расписании общественного транспорта.";
}
@Override
public void dislikeExcuse(String excuse) {
// Добавляет причину в черный список
}
}
Kodu nie można zmienić. Obecny schemat będzie wyglądał następująco: Ta wersja systemu działa tylko z interfejsem Excuse. Nie możesz przepisać kodu: w dużej aplikacji takie zmiany mogą zająć dużo czasu lub złamać logikę aplikacji. Możesz zasugerować wprowadzenie głównego interfejsu i zwiększenie hierarchii: Aby to zrobić, musisz zmienić nazwę interfejsu Excuse
. Jednak w poważnych zastosowaniach dodatkowa hierarchia jest niepożądana: wprowadzenie wspólnego elementu głównego psuje architekturę. Należy zaimplementować klasę pośrednią, która pozwoli na wykorzystanie nowej i starej funkcjonalności przy minimalnych stratach. Krótko mówiąc, potrzebujesz adaptera .
Jak działa wzorzec Adaptera
Adapter to obiekt pośredni, który sprawia, że wywołania metod jednego obiektu są zrozumiałe dla drugiego. Zaimplementujmy adapter dla naszego przykładu i nazwijmy goMiddleware
. Nasz adapter musi implementować interfejs zgodny z jednym z obiektów. Niech będzie Excuse
. Dzięki temu Middleware
może wywoływać metody pierwszego obiektu. Middleware
odbiera wywołania i przekazuje je do drugiego obiektu w kompatybilnym formacie. Tak wygląda implementacja metody Middleware
z metodami generateExcuse
i dislikeExcuse
:
public class Middleware implements Excuse { // 1. Middleware становится совместимым с obiektом WorkExcuse через интерфейс Excuse
private StudentExcuse superStudentExcuse;
public Middleware(StudentExcuse excuse) { // 2. Получаем ссылку на адаптируемый obiekt
this.superStudentExcuse = excuse;
}
@Override
public String generateExcuse() {
return superStudentExcuse.generateExcuse(); // 3. Адаптер реализовывает метод интерфейса
}
@Override
public void dislikeExcuse(String excuse) {
// Метод предварительно помещает отговорку в черный список БД,
// Затем передает ее в метод dislikeExcuse obiektа superStudentExcuse.
}
// Метод likeExcuse появятся позже
}
Testowanie (w kodzie klienta):
public class Test {
public static void main(String[] args) {
Excuse excuse = new WorkExcuse(); // Создаются obiektы классов,
StudentExcuse newExcuse = new SuperStudentExcuse(); // Которые должны быть совмещены.
System.out.println("Обычная причина для работника:");
System.out.println(excuse.generateExcuse());
System.out.println("\n");
Excuse adaptedStudentExcuse = new Middleware(newExcuse); // Оборачиваем новый функционал в obiekt-адаптер
System.out.println("Использование нового функционала с помощью адаптера:");
System.out.println(adaptedStudentExcuse.generateExcuse()); // Адаптер вызывает адаптированный метод
}
}
Wniosek:
Обычная причина для работника:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице.
Нет оправдания моему поступку. Я недостоин этой должности.
Использование нового функционала с помощью адаптера
Niesamowita wymówka, dostosowana do aktualnego stanu pogody, korków czy zakłóceń w rozkładach komunikacji miejskiej. Metoda generateExcuse
po prostu przenosi wywołanie do innego obiektu, bez dodatkowych przekształceń. Metoda dislikeExcuse
wymagała uprzedniego umieszczenia wymówki na czarnej liście bazy danych. Dodatkowe pośrednie przetwarzanie danych jest powodem, dla którego wzór Adapter jest tak lubiany. Ale co z metodą likeExcuse
, która jest w interfejsie Excuse
, ale jej nie ma StudentExcuse
? Ta operacja nie jest obsługiwana w nowej funkcjonalności. W tym przypadku wymyślono wyjątek UnsupportedOperationException
: jest on zgłaszany, jeśli żądana operacja nie jest obsługiwana. Wykorzystajmy to. Tak wygląda implementacja nowej klasy Middleware
:
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("Метод likeExcuse не поддерживается в новом функционале");
}
@Override
public void dislikeExcuse(String excuse) {
// Метод обращается за дополнительной информацией к БД,
// Затем передает ее в метод dislikeExcuse obiektа superStudentExcuse.
}
}
Na pierwszy rzut oka rozwiązanie to nie wydaje się udane, jednak symulowanie funkcjonalności może prowadzić do bardziej złożonej sytuacji. Jeśli klient jest uważny i adapter jest dobrze udokumentowany, to rozwiązanie jest do przyjęcia.
Kiedy używać adaptera
-
Jeśli chcesz użyć klasy innej firmy, ale jej interfejs nie jest kompatybilny z aplikacją główną. Powyższy przykład pokazuje, jak tworzony jest obiekt podkładki, który otacza wywołania w formacie zrozumiałym dla obiektu docelowego.
-
Gdy kilka istniejących podklas musi mieć wspólną funkcjonalność. Zamiast dodatkowych podklas (ich utworzenie będzie prowadzić do powielania kodu), lepiej zastosować adapter.
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 i w przypadku dużej liczby niekompatybilnych punktów ich liczba może urosnąć do niekontrolowanych rozmiarów.Nie mylić z fasadą i dekoratorem
Po powierzchownym zbadaniu Adapter można pomylić z wzorami Fasada i Dekorator. Różnica między adapterem a fasadą polega na tym, że fasada wprowadza nowy interfejs i otacza cały podsystem. Cóż, Dekorator, w przeciwieństwie do Adaptera, zmienia sam obiekt, a nie interfejs.Algorytm realizacji krok po kroku
-
Najpierw upewnij się, że istnieje problem, który ten wzór może rozwiązać.
-
Zdefiniuj interfejs klienta, w imieniu którego będzie używana inna klasa.
-
Zaimplementuj klasę adaptera w oparciu o interfejs zdefiniowany w poprzednim kroku.
-
W klasie adaptera utwórz pole przechowujące referencję do obiektu. To odwołanie jest przekazywane w konstruktorze.
-
Zaimplementuj wszystkie metody interfejsu klienta w adapterze. Metoda może:
-
Przekaż połączenie bez modyfikacji;
-
Zmień dane, zwiększ/zmniejsz liczbę wywołań metody docelowej, dalej rozszerzaj skład danych itp.
-
W ostateczności, jeśli dana metoda jest niezgodna, zgłoś wyjątek UnsupportedOperationException, który należy ściśle udokumentować.
-
-
Jeśli aplikacja korzysta z adaptera wyłącznie poprzez interfejs klienta (jak w powyższym przykładzie), umożliwi to bezproblemową rozbudowę adapterów w przyszłości.
GO TO FULL VERSION