CodeGym /Java-Blog /Random-DE /Welche Probleme löst das Adapterentwurfsmuster?
Autor
Artem Divertitto
Senior Android Developer at United Tech

Welche Probleme löst das Adapterentwurfsmuster?

Veröffentlicht in der Gruppe Random-DE
Die Softwareentwicklung wird durch inkompatible Komponenten, die zusammenarbeiten müssen, erschwert. Wenn Sie beispielsweise eine neue Bibliothek in eine alte, in früheren Java-Versionen geschriebene Plattform integrieren müssen, stoßen Sie möglicherweise auf inkompatible Objekte bzw. inkompatible Schnittstellen. Welche Probleme löst das Adapterentwurfsmuster?  - 1Was ist in diesem Fall zu tun? Den Code umschreiben? Das können wir nicht tun, da die Analyse des Systems viel Zeit in Anspruch nehmen würde oder die interne Logik der Anwendung verletzt würde. Um dieses Problem zu lösen, wurde das Adaptermuster erstellt. Es hilft Objekten mit inkompatiblen Schnittstellen bei der Zusammenarbeit. Mal sehen, wie man es benutzt!

Mehr zum Problem

Zunächst simulieren wir das Verhalten des alten Systems. Angenommen, es generiert Ausreden dafür, dass man zu spät zur Arbeit oder zur Schule kommt. Zu diesem Zweck verfügt es über eine ExcuseSchnittstelle mit den Methoden generateExcuse(), likeExcuse()und dislikeExcuse().

public interface Excuse {
   String generateExcuse();
   void likeExcuse(String excuse);
   void dislikeExcuse(String excuse);
}
Die WorkExcuseKlasse implementiert diese Schnittstelle:

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
   }
}
Testen wir unser Beispiel:

Excuse excuse = new WorkExcuse();
System.out.println(excuse.generateExcuse());
Ausgang:

"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.
Stellen Sie sich nun vor, Sie hätten einen Dienst zum Generieren von Ausreden gestartet, Statistiken gesammelt und festgestellt, dass die meisten Ihrer Benutzer Universitätsstudenten sind. Um diese Gruppe besser bedienen zu können, haben Sie einen anderen Entwickler gebeten, ein System zu entwickeln, das Ausreden speziell für Universitätsstudenten generiert. Das Entwicklungsteam führte Marktforschung durch, ordnete Ausreden ein, schaltete künstliche Intelligenz ein und integrierte den Dienst in Verkehrsmeldungen, Wetterberichte usw. Jetzt haben Sie eine Bibliothek zum Generieren von Ausreden für Universitätsstudenten, aber sie hat eine andere Schnittstelle: StudentExcuse.

public interface StudentExcuse {
   String generateExcuse();
   void dislikeExcuse(String excuse);
}
Diese Schnittstelle verfügt über zwei Methoden: generateExcuse, die eine Entschuldigung generiert, und dislikeExcuse, die verhindert, dass die Entschuldigung in Zukunft erneut erscheint. Die Drittanbieter-Bibliothek kann nicht bearbeitet werden, d. h. Sie können ihren Quellcode nicht ändern. Was wir jetzt haben, ist ein System mit zwei Klassen, die die ExcuseSchnittstelle implementieren, und eine Bibliothek mit einer SuperStudentExcuseKlasse, die die StudentExcuseSchnittstelle implementiert:

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
   }
}
Der Code kann nicht geändert werden. Die aktuelle Klassenhierarchie sieht folgendermaßen aus: Welche Probleme löst das Adapterentwurfsmuster?  - 2Diese Version des Systems funktioniert nur mit der Excuse-Schnittstelle. Sie können den Code nicht neu schreiben: In einer großen Anwendung kann die Durchführung solcher Änderungen zu einem langwierigen Prozess werden oder die Logik der Anwendung zerstören. Wir könnten eine Basisschnittstelle einführen und die Hierarchie erweitern: Welche Probleme löst das Adapterentwurfsmuster?  - 3Dazu müssen wir die ExcuseSchnittstelle umbenennen. Bei seriösen Anwendungen ist die zusätzliche Hierarchie jedoch unerwünscht: Die Einführung eines gemeinsamen Stammelements zerstört die Architektur. Sie sollten eine Zwischenklasse implementieren, die es uns ermöglicht, sowohl die neue als auch die alte Funktionalität mit minimalen Verlusten zu nutzen. Kurz gesagt, Sie benötigen einen Adapter .

Das Prinzip hinter dem Adaptermuster

Ein Adapter ist ein Zwischenobjekt, das es ermöglicht, dass die Methodenaufrufe eines Objekts von einem anderen verstanden werden. Lassen Sie uns für unser Beispiel einen Adapter implementieren und ihn nennen Middleware. Unser Adapter muss eine Schnittstelle implementieren, die mit einem der Objekte kompatibel ist. Lass es sein Excuse. Dadurch können Middlewaredie Methoden des ersten Objekts aufgerufen werden. Middlewarenimmt Anrufe entgegen und leitet sie kompatible an das zweite Objekt weiter. Hier ist die MiddlewareImplementierung mit den Methoden generateExcuseund 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
}
Testen (im Client-Code):

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
   }
}
Ausgang:

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:
Eine unglaubliche Ausrede, angepasst an die aktuellen Wetterbedingungen, Staus oder Verspätungen im öffentlichen Nahverkehr. Die generateExcuseMethode leitet den Aufruf einfach ohne weitere Änderungen an ein anderes Objekt weiter. Die dislikeExcuseMethode erforderte, dass wir zunächst die Ausrede auf die schwarze Liste setzten. Die Fähigkeit, eine Zwischendatenverarbeitung durchzuführen, ist einer der Gründe, warum Menschen das Adaptermuster lieben. Aber was ist mit der likeExcuseMethode, die Teil der ExcuseSchnittstelle, aber nicht Teil der StudentExcuseSchnittstelle ist? Die neue Funktionalität unterstützt diesen Vorgang nicht. Der UnsupportedOperationExceptionwurde für diese Situation erfunden. Es wird ausgelöst, wenn der angeforderte Vorgang nicht unterstützt wird. Nutzen wir es. So Middlewaresieht die neue Implementierung der Klasse aus:

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.
   }
}
Auf den ersten Blick scheint diese Lösung nicht sehr gut zu sein, aber die Nachahmung der Funktionalität kann die Situation verkomplizieren. Wenn der Kunde aufmerksam ist und der Adapter gut dokumentiert ist, ist eine solche Lösung akzeptabel.

Wann sollte ein Adapter verwendet werden?

  1. Wenn Sie eine Drittanbieterklasse verwenden müssen, deren Schnittstelle jedoch nicht mit der Hauptanwendung kompatibel ist. Das obige Beispiel zeigt, wie ein Adapterobjekt erstellt wird, das Aufrufe in ein Format umschließt, das ein Zielobjekt verstehen kann.

  2. Wenn mehrere vorhandene Unterklassen eine gemeinsame Funktionalität benötigen. Anstatt zusätzliche Unterklassen zu erstellen (was zu einer Duplizierung des Codes führt), ist es besser, einen Adapter zu verwenden.

Vorteile und Nachteile

Vorteil: Der Adapter verbirgt vor dem Client die Details der Verarbeitung von Anfragen von einem Objekt zu einem anderen. Der Clientcode kümmert sich nicht um die Formatierung von Daten oder die Verarbeitung von Aufrufen der Zielmethode. Es ist zu kompliziert und Programmierer sind faul :) Nachteil: Die Codebasis des Projekts wird durch zusätzliche Klassen kompliziert. Wenn Sie viele inkompatible Schnittstellen haben, kann die Anzahl zusätzlicher Klassen unüberschaubar werden.

Verwechseln Sie einen Adapter nicht mit einer Fassade oder einem Dekorateur

Bei nur oberflächlicher Betrachtung könnte ein Adapter mit den Fassaden- und Dekorationsmustern verwechselt werden. Der Unterschied zwischen einem Adapter und einer Fassade besteht darin, dass eine Fassade eine neue Schnittstelle einführt und das gesamte Subsystem umschließt. Und im Gegensatz zu einem Adapter verändert ein Dekorateur das Objekt selbst und nicht die Schnittstelle.

Schritt-für-Schritt-Algorithmus

  1. Stellen Sie zunächst sicher, dass Sie ein Problem haben, das dieses Muster lösen kann.

  2. Definieren Sie die Client-Schnittstelle, die für die indirekte Interaktion mit inkompatiblen Objekten verwendet wird.

  3. Lassen Sie die Adapterklasse die im vorherigen Schritt definierte Schnittstelle erben.

  4. Erstellen Sie in der Adapterklasse ein Feld zum Speichern einer Referenz auf das Adaptee-Objekt. Diese Referenz wird an den Konstruktor übergeben.

  5. Implementieren Sie alle Clientschnittstellenmethoden im Adapter. Eine Methode kann:

    • Leiten Sie Anrufe weiter, ohne Änderungen vorzunehmen

    • Ändern oder ergänzen Sie Daten, erhöhen/verringern Sie die Anzahl der Aufrufe der Zielmethode usw.

    • Wenn in extremen Fällen eine bestimmte Methode weiterhin inkompatibel ist, lösen Sie eine UnsupportedOperationException aus. Nicht unterstützte Vorgänge müssen streng dokumentiert werden.

  6. Wenn die Anwendung die Adapterklasse nur über die Client-Schnittstelle verwendet (wie im obigen Beispiel), kann der Adapter in Zukunft problemlos erweitert werden.

Natürlich ist dieses Entwurfsmuster kein Allheilmittel, aber es kann Ihnen dabei helfen, das Problem der Inkompatibilität zwischen Objekten mit unterschiedlichen Schnittstellen elegant zu lösen. Ein Entwickler, der die Grundmuster kennt, ist denjenigen, die nur wissen, wie man Algorithmen schreibt, mehrere Schritte voraus, denn für die Erstellung seriöser Anwendungen sind Entwurfsmuster erforderlich. Die Wiederverwendung von Code ist nicht so schwierig und die Wartung macht Spaß. Das ist alles für heute! Aber wir werden bald weiterhin verschiedene Designmuster kennenlernen :)
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION