Java-Tipp 68: Erfahren Sie, wie Sie das Befehlsmuster in Java implementieren

Entwurfsmuster beschleunigen nicht nur die Entwurfsphase eines objektorientierten Projekts (OO), sondern erhöhen auch die Produktivität des Entwicklungsteams und die Qualität der Software. Ein Befehlsmuster ist ein Objektverhaltensmuster, mit dem wir eine vollständige Entkopplung zwischen Sender und Empfänger erreichen können. (Ein Absender ist ein Objekt, das eine Operation aufruft, und ein Empfänger ist ein Objekt, das die Anforderung zum Ausführen einer bestimmten Operation empfängt. Bei der Entkopplung hat der Absender keine Kenntnis von der ReceiverSchnittstelle der.) Der Begriff Anforderunghier bezieht sich auf den Befehl, der ausgeführt werden soll. Das Befehlsmuster ermöglicht es uns auch zu variieren, wann und wie eine Anforderung erfüllt wird. Daher bietet uns ein Befehlsmuster Flexibilität und Erweiterbarkeit.

In Programmiersprachen wie C werden Funktionszeiger verwendet, um riesige switch-Anweisungen zu eliminieren. (Eine ausführlichere Beschreibung finden Sie unter "Java-Tipp 30: Polymorphismus und Java".) Da Java keine Funktionszeiger hat, können wir das Befehlsmuster zum Implementieren von Rückrufen verwenden. Sie werden dies in Aktion im ersten Codebeispiel unten sehen, das aufgerufen wird TestCommand.java.

Entwickler, die daran gewöhnt sind, Funktionszeiger in einer anderen Sprache zu verwenden, könnten versucht sein, die MethodObjekte der Reflection-API auf dieselbe Weise zu verwenden. In seinem Artikel "Java Reflection" zeigt Ihnen Paul Tremblett beispielsweise, wie Sie mit Reflection Transaktionen implementieren, ohne switch-Anweisungen zu verwenden. Ich habe mich dieser Versuchung widersetzt, da Sun von der Verwendung der Reflection-API abrät, wenn andere Tools, die für die Java-Programmiersprache natürlicher sind, ausreichen. (Unter Ressourcen finden Sie Links zu Trembletts Artikel und zur Tutorial-Seite zu Suns Reflection.) Ihr Programm ist einfacher zu debuggen und zu warten, wenn Sie keine MethodObjekte verwenden. Stattdessen sollten Sie eine Schnittstelle definieren und in den Klassen implementieren, die die erforderliche Aktion ausführen.

Daher schlage ich vor, dass Sie das Befehlsmuster in Kombination mit dem dynamischen Lade- und Bindungsmechanismus von Java verwenden, um Funktionszeiger zu implementieren. (Einzelheiten zum dynamischen Lade- und Bindungsmechanismus von Java finden Sie in James Gosling und Henry McGiltons "The Java Language Environment - A White Paper", aufgeführt in Resources.)

Indem wir dem obigen Vorschlag folgen, nutzen wir den Polymorphismus, der durch die Anwendung eines Befehlsmusters bereitgestellt wird, um riesige switch-Anweisungen zu eliminieren, was zu erweiterbaren Systemen führt. Wir nutzen auch die einzigartigen dynamischen Lade- und Bindungsmechanismen von Java, um ein dynamisches und dynamisch erweiterbares System aufzubauen. Dies wird im folgenden zweiten Codebeispiel veranschaulicht TestTransactionCommand.java.

Das Befehlsmuster verwandelt die Anforderung selbst in ein Objekt. Dieses Objekt kann wie andere Objekte gespeichert und weitergegeben werden. Der Schlüssel zu diesem Muster ist eine CommandSchnittstelle, die eine Schnittstelle zum Ausführen von Operationen deklariert. In ihrer einfachsten Form enthält diese Schnittstelle eine abstrakte executeOperation. Jede konkrete CommandKlasse gibt ein Empfänger-Aktions-Paar an, indem sie Receiverals Instanzvariable gespeichert wird. Es bietet verschiedene Implementierungen der execute()Methode zum Aufrufen der Anforderung. Der Receiverhat die erforderlichen Kenntnisse, um die Anfrage auszuführen.

Abbildung 1 unten zeigt die Switch- eine Aggregation von CommandObjekten. Es hat flipUp()und flipDown()Operationen in seiner Schnittstelle. Switchwird als Aufrufer bezeichnet, da er die Ausführungsoperation in der Befehlsschnittstelle aufruft.

Der konkrete Befehl LightOnCommandimplementiert die executeOperation der Befehlsschnittstelle. Es hat das Wissen, Receiverdie Operation des entsprechenden Objekts aufzurufen . In diesem Fall fungiert es als Adapter. Mit dem Begriff Adapter meine ich, dass das konkrete CommandObjekt ein einfacher Verbinder ist, der das Invokerund das Receivermit verschiedenen Schnittstellen verbindet.

Der Client instanziiert die Befehlsobjekte Invoker, das Receiverund das konkrete Befehlsobjekt.

Abbildung 2, das Sequenzdiagramm, zeigt die Wechselwirkungen zwischen den Objekten. Es zeigt, wie Commanddas Invokervon Receiver(und der von ihm ausgeführten Anforderung) entkoppelt wird. Der Client erstellt einen konkreten Befehl, indem er seinen Konstruktor mit dem entsprechenden Parameter parametrisiert Receiver. Dann speichert es das Commandin der Invoker. Der Invokerruft den konkreten Befehl zurück, der das Wissen hat, die gewünschte Action()Operation auszuführen .

Der Client (Hauptprogramm in der Liste) erstellt ein konkretes CommandObjekt und legt sein fest Receiver. Speichert als InvokerObjekt Switchdas konkrete CommandObjekt. Das Invokergibt eine Anfrage aus, indem es executedas CommandObjekt aufruft . Das konkrete CommandObjekt ruft Operationen auf Receiver, um die Anforderung auszuführen.

Die Schlüsselidee dabei ist, dass sich der konkrete Befehl beim Invokerund registriert und Invokerihn zurückruft und den Befehl auf dem ausführt Receiver.

Beispielcode für Befehlsmuster

Schauen wir uns ein einfaches Beispiel an, das den über das Befehlsmuster erzielten Rückrufmechanismus veranschaulicht.

Das Beispiel zeigt a Fanund a Light. Unser Ziel ist es, ein SwitchObjekt zu entwickeln , das jedes Objekt ein- oder ausschalten kann. Wir sehen, dass die Fanund die Lightunterschiedliche Schnittstellen haben, was bedeutet Switch, dass die von der ReceiverSchnittstelle unabhängig sein müssen oder keine Kenntnis von der Code> Empfängerschnittstelle haben. Um dieses Problem zu lösen, müssen wir jedes der Switchs mit dem entsprechenden Befehl parametrisieren . Offensichtlich haben die Switchmit dem verbundenen Lighteinen anderen Befehl als die Switchmit dem verbundenen Fan. Die CommandKlasse muss abstrakt oder eine Schnittstelle sein, damit dies funktioniert.

Wenn der Konstruktor für a Switchaufgerufen wird, wird er mit den entsprechenden Befehlen parametrisiert. Die Befehle werden als private Variablen der gespeichert Switch.

Wenn die Operationen flipUp()und flipDown()aufgerufen werden, geben sie einfach den entsprechenden Befehl an ein execute( ). Der Switchwird keine Ahnung haben, was als Ergebnis eines Anrufs passiert execute( ).

TestCommand.java-Klasse Fan {public void startRotate () {System.out.println ("Fan dreht sich"); } public void stopRotate () {System.out.println ("Lüfter dreht sich nicht"); }} Klasse Light {public void turnOn () {System.out.println ("Licht ist an"); } public void turnOff () {System.out.println ("Licht ist aus"); }} class Switch {privater Befehl UpCommand, DownCommand; öffentlicher Schalter (Befehl hoch, Befehl runter) {UpCommand = Up; // konkreter Befehl registriert sich beim Aufrufer DownCommand = Down; } void flipUp () {// Der Aufrufer ruft einen konkreten Befehl zurück, der den Befehl auf dem UpCommand des Empfängers ausführt. ausführen ( ) ; } void flipDown () {DownCommand. ausführen ( ); }} Klasse LightOnCommand implementiert Command {private Light myLight; public LightOnCommand (Light L) {myLight = L;} public void execute () {myLight. anmachen( ); }} Klasse LightOffCommand implementiert Command {private Light myLight; public LightOffCommand (Light L) {myLight = L; } public void execute () {myLight. ausschalten( ); }} Klasse FanOnCommand implementiert Command {private Fan myFan; öffentlicher FanOnCommand (Fan F) {myFan = F; } public void execute () {myFan. startRotate (); }} Klasse FanOffCommand implementiert Command {private Fan myFan; public FanOffCommand (Fan F) {myFan = F; } public void execute () {myFan. stopRotate (); }} öffentliche Klasse TestCommand {public static void main (String [] args) {Light testLight = new Light (); LightOnCommand testLOC = neuer LightOnCommand (testLight); LightOffCommand testLFC = neuer LightOffCommand (testLight); Switch testSwitch = neuer Switch (testLOC, testLFC); testSwitch.flipUp (); testSwitch.flipDown ();Lüfter testFan = neuer Lüfter (); FanOnCommand foc = neuer FanOnCommand (testFan); FanOffCommand ffc = neuer FanOffCommand (testFan); Schalter ts = neuer Schalter (foc, ffc); ts.flipUp (); ts.flipDown (); }} Command.java öffentliche Schnittstelle Command {public abstract void execute (); }}

Beachten Sie im Codebeispiel oben , dass das Befehlsmuster abkoppelt vollständig das Objekt, das die Operation aufruft - (Switch )- von denen das Wissen, die sie auszuführen - Lightund Fan. Dies gibt uns viel Flexibilität: Das Objekt, das eine Anfrage ausstellt, muss nur wissen, wie es sie ausgibt. Es muss nicht wissen, wie die Anfrage ausgeführt wird.

Befehlsmuster zum Implementieren von Transaktionen

Ein Befehlsmuster wird auch als Aktions- oder Transaktionsmuster bezeichnet. Betrachten wir einen Server, der Transaktionen akzeptiert und verarbeitet, die von Clients über eine TCP / IP-Socket-Verbindung bereitgestellt werden. Diese Transaktionen bestehen aus einem Befehl, gefolgt von null oder mehr Argumenten.

Entwickler verwenden möglicherweise eine switch-Anweisung mit einem Fall für jeden Befehl. Die Verwendung von SwitchAnweisungen während der Codierung ist ein Zeichen für schlechtes Design während der Entwurfsphase eines objektorientierten Projekts. Befehle stellen eine objektorientierte Methode zur Unterstützung von Transaktionen dar und können zur Lösung dieses Entwurfsproblems verwendet werden.

Im Client-Code des Programms TestTransactionCommand.javasind alle Anforderungen in das generische TransactionCommandObjekt eingekapselt . Der TransactionCommandKonstruktor wird vom Client erstellt und beim registriert CommandManager. Die Anforderungen in der Warteschlange können zu unterschiedlichen Zeiten ausgeführt werden, indem Sie das anrufen runCommands(), was uns viel Flexibilität gibt. Es gibt uns auch die Möglichkeit, Befehle zu einem zusammengesetzten Befehl zusammenzusetzen. Ich habe auch CommandArgument, CommandReceiverund CommandManagerKlassen und Unterklassen von TransactionCommand- nämlich AddCommandund SubtractCommand. Es folgt eine Beschreibung jeder dieser Klassen:

  • CommandArgumentist eine Hilfsklasse, die die Argumente des Befehls speichert. Es kann umgeschrieben werden, um die Übergabe einer großen oder variablen Anzahl von Argumenten eines beliebigen Typs zu vereinfachen.

  • CommandReceiver implementiert alle Befehlsverarbeitungsmethoden und wird als Singleton-Muster implementiert.

  • CommandManagerist der Aufrufer und Switchentspricht dem vorherigen Beispiel. Es speichert das generische TransactionCommandObjekt in seiner privaten myCommandVariablen. Wenn runCommands( )es aufgerufen wird, ruft es das execute( )des entsprechenden TransactionCommandObjekts auf.

In Java ist es möglich, die Definition einer Klasse anhand einer Zeichenfolge mit ihrem Namen nachzuschlagen. Während des execute ( )Betriebs der TransactionCommandKlasse berechne ich den Klassennamen und verknüpfe ihn dynamisch mit dem laufenden System. Das heißt, Klassen werden nach Bedarf im laufenden Betrieb geladen. Ich verwende die Namenskonvention, den durch die Zeichenfolge "Command" verketteten Befehlsnamen als Namen der Transaktionsbefehlsunterklasse, damit sie dynamisch geladen werden kann.

Beachten Sie, dass das vom zurückgegebene ClassObjekt newInstance( )in den entsprechenden Typ umgewandelt werden muss. Dies bedeutet, dass die neue Klasse entweder eine Schnittstelle implementieren oder eine vorhandene Klasse unterklassifizieren muss, die dem Programm zur Kompilierungszeit bekannt ist. In diesem Fall ist Commanddies kein Problem , da wir die Schnittstelle implementieren .