Einführung in Designmuster, Teil 2: Gang-of-Four-Klassiker überarbeitet

In Teil 1 dieser dreiteiligen Serie, in der Entwurfsmuster vorgestellt wurden, bezog ich mich auf Entwurfsmuster: Elemente wiederverwendbaren objektorientierten Entwurfs . Dieser Klassiker wurde von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides geschrieben, die zusammen als Gang of Four bekannt waren. Wie die meisten Leser wissen, präsentiert Design Patterns 23 Software-Designmuster, die in die in Teil 1 beschriebenen Kategorien passen: Kreativ, strukturell und verhaltensbezogen.

Entwerfen Sie Muster in JavaWorld

Die Java-Entwurfsmusterserie von David Geary ist eine meisterhafte Einführung in viele der Gang of Four-Muster in Java-Code.

Design Patterns ist für Softwareentwickler eine kanonische Lektüre, aber viele neue Programmierer werden durch das Referenzformat und den Umfang herausgefordert. Jedes der 23 Muster wird ausführlich in einem Vorlagenformat beschrieben, das aus 13 Abschnitten besteht, die sehr viel zu verdauen sind. Eine weitere Herausforderung für neue Java-Entwickler besteht darin, dass die Gang of Four-Muster aus der objektorientierten Programmierung stammen. Beispiele basieren auf C ++ und Smalltalk, nicht auf Java-Code.

In diesem Tutorial entpacke ich zwei der häufig verwendeten Muster - Strategie und Besucher - aus Sicht eines Java-Entwicklers. Strategie ist ein ziemlich einfaches Muster, das als Beispiel dafür dient, wie Sie Ihre Füße mit den GoF-Designmustern im Allgemeinen nass machen können. Der Besucher ist komplexer und von mittlerer Reichweite. Ich beginne mit einem Beispiel, das den Doppelversandmechanismus entmystifizieren soll, der ein wichtiger Bestandteil des Besuchermusters ist. Dann werde ich das Besuchermuster in einem Compiler-Anwendungsfall demonstrieren.

Das Befolgen meiner Beispiele hier soll Ihnen helfen, die anderen GoF-Muster selbst zu erkunden und zu verwenden. Darüber hinaus werde ich Tipps geben, wie Sie das Buch Gang of Four optimal nutzen können, und mit einer Zusammenfassung der Kritik an der Verwendung von Entwurfsmustern in der Softwareentwicklung abschließen. Diese Diskussion könnte besonders für Entwickler relevant sein, die neu in der Programmierung sind.

Auspackstrategie

Mit dem Strategiemuster können Sie eine Reihe von Algorithmen definieren, z. B. für die Sortierung, Textkomposition oder Layoutverwaltung. Mit Strategy können Sie außerdem jeden Algorithmus in einer eigenen Klasse kapseln und austauschbar machen. Jeder gekapselte Algorithmus wird als Strategie bezeichnet . Zur Laufzeit wählt ein Client den geeigneten Algorithmus für seine Anforderungen.

Was ist ein Kunde?

Ein Client ist eine Software, die mit einem Entwurfsmuster interagiert. Obwohl es sich normalerweise um ein Objekt handelt, kann ein Client auch Code innerhalb der public static void main(String[] args)Methode einer Anwendung sein .

Im Gegensatz zum Decorator-Muster, bei dem es darum geht, die Haut oder das Erscheinungsbild eines Objekts zu ändern , konzentriert sich Strategy auf die Änderung der Eingeweide des Objekts , dh seines veränderlichen Verhaltens. Mit Strategie können Sie die Verwendung mehrerer bedingter Anweisungen vermeiden, indem Sie bedingte Zweige in ihre eigenen Strategieklassen verschieben. Diese Klassen stammen häufig aus einer abstrakten Oberklasse, auf die der Client verweist und die er zur Interaktion mit einer bestimmten Strategie verwendet.

Von einer abstrakten Perspektive beinhaltet Strategie Strategy, und Typen.ConcreteStrategyxContext

Strategie

Strategybietet eine gemeinsame Schnittstelle zu allen unterstützten Algorithmen. Listing 1 zeigt die StrategySchnittstelle.

Listing 1. void execute (int x) muss von allen konkreten Strategien implementiert werden

public interface Strategy { public void execute(int x); }

Wenn konkrete Strategien nicht mit allgemeinen Daten parametrisiert werden, können Sie sie über die Java- interfaceFunktion implementieren . Wo sie parametrisiert sind, würden Sie stattdessen eine abstrakte Klasse deklarieren. Beispielsweise teilen Strategien zur Ausrichtung von Recht, Ausrichtung zur Mitte und Ausrichtung von Text das Konzept einer Breite, in der die Textausrichtung durchgeführt werden soll. Sie würden diese Breite also in der abstrakten Klasse deklarieren .

ConcreteStrategy x

Jedes implementiert die gemeinsame Schnittstelle und bietet eine Algorithmusimplementierung. Listing 2 implementiert die Schnittstelle von Listing 1 , um eine bestimmte konkrete Strategie zu beschreiben.ConcreteStrategyxStrategy

Listing 2. ConcreteStrategyA führt einen Algorithmus aus

public class ConcreteStrategyA implements Strategy { @Override public void execute(int x) { System.out.println("executing strategy A: x = "+x); } }

Die void execute(int x)Methode in Listing 2 identifiziert eine bestimmte Strategie. Stellen Sie sich diese Methode als Abstraktion für etwas Nützlicheres vor, z. B. eine bestimmte Art von Sortieralgorithmus (z. B. Blasensortierung, Einfügesortierung oder Schnellsortierung) oder eine bestimmte Art von Layout-Manager (z. B. Flusslayout, Rahmenlayout oder Gitterstruktur).

Listing 3 enthält eine zweite StrategyImplementierung.

Listing 3. ConcreteStrategyB führt einen anderen Algorithmus aus

public class ConcreteStrategyB implements Strategy { @Override public void execute(int x) { System.out.println("executing strategy B: x = "+x); } }

Kontext

Contextliefert den Kontext, in dem die konkrete Strategie aufgerufen wird. Die Listen 2 und 3 zeigen Daten, die über einen Methodenparameter von einem Kontext an eine Strategie übergeben werden. Da eine generische Strategie-Schnittstelle von allen konkreten Strategien gemeinsam genutzt wird, erfordern einige von ihnen möglicherweise nicht alle Parameter. Um unnötige Parameter zu vermeiden (insbesondere wenn viele verschiedene Arten von Argumenten nur an wenige konkrete Strategien übergeben werden), können Sie stattdessen einen Verweis auf den Kontext übergeben.

Anstatt einen Kontextverweis auf die Methode zu übergeben, können Sie ihn in der abstrakten Klasse speichern und Ihre Methodenaufrufe parameterlos machen. Der Kontext müsste jedoch eine umfangreichere Schnittstelle angeben, die den Vertrag für den einheitlichen Zugriff auf Kontextdaten enthält. Das Ergebnis ist, wie in Listing 4 gezeigt, eine engere Kopplung zwischen Strategien und ihrem Kontext.

Listing 4. Der Kontext wird mit einer ConcreteStrategyx-Instanz konfiguriert

class Context { private Strategy strategy; public Context(Strategy strategy) { setStrategy(strategy); } public void executeStrategy(int x) { strategy.execute(x); } public void setStrategy(Strategy strategy) { this.strategy = strategy; } }

Die ContextKlasse in Listing 4 speichert eine Strategie beim Erstellen, stellt eine Methode zum anschließenden Ändern der Strategie und eine andere Methode zum Ausführen der aktuellen Strategie bereit. Mit Ausnahme eine Strategie zu dem Konstruktor übergibt, kann dieses Muster in der java.awt .Behälter Klasse zu sehen, deren void setLayout(LayoutManager mgr)und void doLayout()Methoden angeben und die Layout - Manager - Strategie auszuführen.

StrategyDemo

Wir brauchen einen Kunden, der die vorherigen Typen demonstriert. Listing 5 zeigt eine StrategyDemoClient-Klasse.

Listing 5. StrategyDemo

public class StrategyDemo { public static void main(String[] args) { Context context = new Context(new ConcreteStrategyA()); context.executeStrategy(1); context.setStrategy(new ConcreteStrategyB()); context.executeStrategy(2); } }

Eine konkrete Strategie ist einer ContextInstanz zugeordnet, wenn der Kontext erstellt wird. Die Strategie kann anschließend über einen Kontextmethodenaufruf geändert werden.

Wenn Sie diese Klassen kompilieren und ausführen StrategyDemo, sollten Sie die folgende Ausgabe beachten:

executing strategy A: x = 1 executing strategy B: x = 2

Überarbeitung des Besuchermusters

Besucher ist das endgültige Software-Entwurfsmuster, das in Entwurfsmustern angezeigt wird . Obwohl dieses Verhaltensmuster aus alphabetischen Gründen als letztes im Buch dargestellt wird, glauben einige, dass es aufgrund seiner Komplexität das letzte sein sollte. Neulinge im Besucherbereich haben häufig Probleme mit diesem Software-Designmuster.

Wie in Entwurfsmuster erläutert , können Sie mit einem Besucher Klassen Operationen hinzufügen, ohne sie zu ändern. Dies ist ein bisschen magisch, das durch die sogenannte Doppelversandtechnik erleichtert wird. Um das Besuchermuster zu verstehen, müssen wir zuerst den doppelten Versand verdauen.

Was ist Doppelversand?

Java and many other languages support polymorphism (many shapes) via a technique known as dynamic dispatch, in which a message is mapped to a specific sequence of code at runtime. Dynamic dispatch is classified as either single dispatch or multiple dispatch:

  • Single dispatch: Given a class hierarchy where each class implements the same method (that is, each subclass overrides the previous class's version of the method), and given a variable that's assigned an instance of one of these classes, the type can be figured out only at runtime. For example, suppose each class implements method print(). Suppose too that one of these classes is instantiated at runtime and its variable assigned to variable a. When the Java compiler encounters a.print();, it can only verify that a's type contains a print() method. It doesn't know which method to call. At runtime, the virtual machine examines the reference in variable a and figures out the actual type in order to call the right method. This situation, in which an implementation is based on a single type (the type of the instance), is known as single dispatch.
  • Multiple dispatch: Unlike in single dispatch, where a single argument determines which method of that name to invoke, multiple dispatch uses all of its arguments. In other words, it generalizes dynamic dispatch to work with two or more objects. (Note that the argument in single dispatch is typically specified with a period separator to the left of the method name being called, such as the a in a.print().)

Finally, double dispatch is a special case of multiple dispatch in which the runtime types of two objects are involved in the call. Although Java supports single dispatch, it doesn't support double dispatch directly. But we can simulate it.

Do we over-rely on double dispatch?

Blogger Derek Greer believes that using double dispatch may indicate a design issue, which could impact an application's maintainability. Read Greer's "Double dispatch is a code smell" blog post and associated comments for details.

Simulating double dispatch in Java code

Wikipedia's entry on double dispatch provides a C++-based example that shows it to be more than function overloading. In Listing 6, I present the Java equivalent.

Listing 6. Double dispatch in Java code

public class DDDemo { public static void main(String[] args) { Asteroid theAsteroid = new Asteroid(); SpaceShip theSpaceShip = new SpaceShip(); ApolloSpacecraft theApolloSpacecraft = new ApolloSpacecraft(); theAsteroid.collideWith(theSpaceShip); theAsteroid.collideWith(theApolloSpacecraft); System.out.println(); ExplodingAsteroid theExplodingAsteroid = new ExplodingAsteroid(); theExplodingAsteroid.collideWith(theSpaceShip); theExplodingAsteroid.collideWith(theApolloSpacecraft); System.out.println(); Asteroid theAsteroidReference = theExplodingAsteroid; theAsteroidReference.collideWith(theSpaceShip); theAsteroidReference.collideWith(theApolloSpacecraft); System.out.println(); SpaceShip theSpaceShipReference = theApolloSpacecraft; theAsteroid.collideWith(theSpaceShipReference); theAsteroidReference.collideWith(theSpaceShipReference); System.out.println(); theSpaceShipReference = theApolloSpacecraft; theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.collideWith(theAsteroid); theSpaceShipReference.collideWith(theAsteroidReference); } } class SpaceShip { void collideWith(Asteroid inAsteroid) { inAsteroid.collideWith(this); } } class ApolloSpacecraft extends SpaceShip { void collideWith(Asteroid inAsteroid) { inAsteroid.collideWith(this); } } class Asteroid { void collideWith(SpaceShip s) { System.out.println("Asteroid hit a SpaceShip"); } void collideWith(ApolloSpacecraft as) { System.out.println("Asteroid hit an ApolloSpacecraft"); } } class ExplodingAsteroid extends Asteroid { void collideWith(SpaceShip s) { System.out.println("ExplodingAsteroid hit a SpaceShip"); } void collideWith(ApolloSpacecraft as) { System.out.println("ExplodingAsteroid hit an ApolloSpacecraft"); } }

Listing 6 follows its C++ counterpart as closely as possible. The final four lines in the main() method along with the void collideWith(Asteroid inAsteroid) methods in SpaceShip and ApolloSpacecraft demonstrate and simulate double dispatch.

Consider the following excerpt from the end of main():

theSpaceShipReference = theApolloSpacecraft; theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.collideWith(theAsteroid); theSpaceShipReference.collideWith(theAsteroidReference);

The third and fourth lines use single dispatch to figure out the correct collideWith() method (in SpaceShip or ApolloSpacecraft) to invoke. This decision is made by the virtual machine based on the type of the reference stored in theSpaceShipReference.

Von innen collideWith(), inAsteroid.collideWith(this);verwendet einzelnen Dispatch um herauszufinden die richtige Klasse ( Asteroidoder ExplodingAsteroid) die gewünschte enthält collideWith()Methode. Da Asteroidund ExplodingAsteroidüberladen collideWith(), wird der Typ des Arguments this( SpaceShipoder ApolloSpacecraft) verwendet, um die richtige collideWith()aufzurufende Methode zu unterscheiden.

Und damit haben wir den doppelten Versand erreicht. Zur Erinnerung riefen wir zuerst collideWith()in SpaceShipoder ApolloSpacecraft, und dann ihr Argument verwendet und thiseine der rufen collideWith()Methoden in Asteroidoder ExplodingAsteroid.

Wenn Sie ausführen DDDemo, sollten Sie die folgende Ausgabe beachten: