Zustandsentwurfsmuster in Java

1. Übersicht

In diesem Tutorial stellen wir eines der Verhaltens-GoF-Entwurfsmuster vor - das Statusmuster.

Zunächst geben wir einen Überblick über den Zweck und erläutern das Problem, das es zu lösen versucht. Dann werfen wir einen Blick auf das UML-Diagramm des Staates und die Implementierung des praktischen Beispiels.

2. Zustandsentwurfsmuster

Die Hauptidee des Zustandsmusters besteht darin , dem Objekt zu erlauben, sein Verhalten zu ändern, ohne seine Klasse zu ändern. Durch die Implementierung sollte der Code auch ohne viele if / else-Anweisungen sauberer bleiben.

Stellen Sie sich vor, wir haben ein Paket, das an eine Poststelle gesendet wird. Das Paket selbst kann bestellt, dann an eine Poststelle geliefert und schließlich von einem Kunden empfangen werden. Abhängig vom aktuellen Status möchten wir nun den Lieferstatus ausdrucken.

Der einfachste Ansatz wäre, einige boolesche Flags hinzuzufügen und einfache if / else-Anweisungen in jeder unserer Methoden in der Klasse anzuwenden. Das wird es in einem einfachen Szenario nicht viel komplizieren. Dies kann jedoch unseren Code komplizieren und verschmutzen, wenn mehr Zustände verarbeitet werden, was zu noch mehr if / else-Anweisungen führt.

Außerdem würde die gesamte Logik für jeden der Zustände auf alle Methoden verteilt sein. Hier könnte nun das Statusmuster verwendet werden. Dank des Entwurfsmusters für den Status können wir die Logik in dedizierten Klassen zusammenfassen, das Prinzip der Einzelverantwortung und das Prinzip der offenen / geschlossenen Anwendung anwenden, saubereren und wartbareren Code haben.

3. UML-Diagramm

Im UML - Diagramm sehen wir , dass Context - Klasse einen zugehörigen hat Staat , die während der Programmausführung ändern wird.

Unser Kontext wird das Verhalten an die staatliche Implementierung delegieren. Mit anderen Worten, alle eingehenden Anfragen werden von der konkreten Umsetzung des Staates bearbeitet.

Wir sehen, dass die Logik getrennt ist und das Hinzufügen neuer Zustände einfach ist - es kommt darauf an, bei Bedarf eine weitere Zustandsimplementierung hinzuzufügen .

4. Implementierung

Lassen Sie uns unsere Anwendung entwerfen. Wie bereits erwähnt, kann das Paket bestellt, geliefert und empfangen werden, daher werden wir drei Zustände und die Kontextklasse haben.

Definieren wir zunächst unseren Kontext, der eine Paketklasse sein wird :

public class Package { private PackageState state = new OrderedState(); // getter, setter public void previousState() { state.prev(this); } public void nextState() { state.next(this); } public void printStatus() { state.printStatus(); } }

Wie wir sehen können, enthält es eine Referenz zum Verwalten des Status. Beachten Sie die Methoden previousState (), nextState () und printStatus () , bei denen wir den Job an das Statusobjekt delegieren. Die Zustände werden miteinander verknüpft und jeder Zustand legt einen anderen fest, basierend auf dieser Referenz, die an beide Methoden übergeben wird.

Der Client interagiert mit der Package- Klasse, muss sich jedoch nicht mit dem Festlegen der Status befassen. Der Client muss lediglich zum nächsten oder vorherigen Status wechseln.

Als nächstes haben wir den PackageState, der drei Methoden mit den folgenden Signaturen hat:

public interface PackageState { void next(Package pkg); void prev(Package pkg); void printStatus(); }

Diese Schnittstelle wird von jeder konkreten Zustandsklasse implementiert.

Der erste konkrete Zustand ist OrderedState :

public class OrderedState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new DeliveredState()); } @Override public void prev(Package pkg) { System.out.println("The package is in its root state."); } @Override public void printStatus() { System.out.println("Package ordered, not delivered to the office yet."); } }

Hier zeigen wir auf den nächsten Zustand, der nach der Bestellung des Pakets eintreten wird. Der geordnete Zustand ist unser Stammzustand und wir markieren ihn explizit. Wir können in beiden Methoden sehen, wie der Übergang zwischen Zuständen behandelt wird.

Werfen wir einen Blick auf die DeliveredState- Klasse:

public class DeliveredState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new ReceivedState()); } @Override public void prev(Package pkg) { pkg.setState(new OrderedState()); } @Override public void printStatus() { System.out.println("Package delivered to post office, not received yet."); } }

Wieder sehen wir die Verbindung zwischen den Staaten. Das Paket ändert seinen Status von bestellt zu geliefert, die Nachricht in printStatus () ändert sich ebenfalls.

Der letzte Status ist ReceivedState :

public class ReceivedState implements PackageState { @Override public void next(Package pkg) { System.out.println("This package is already received by a client."); } @Override public void prev(Package pkg) { pkg.setState(new DeliveredState()); } }

Hier erreichen wir den letzten Zustand, wir können nur auf den vorherigen Zustand zurücksetzen.

Wir sehen bereits, dass es eine gewisse Auszahlung gibt, da ein Staat über den anderen Bescheid weiß. Wir machen sie eng miteinander verbunden.

5. Testen

Mal sehen, wie sich die Implementierung verhält. Lassen Sie uns zunächst überprüfen, ob die Setup-Übergänge wie erwartet funktionieren:

@Test public void givenNewPackage_whenPackageReceived_thenStateReceived() { Package pkg = new Package(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(DeliveredState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(ReceivedState.class)); }

Überprüfen Sie dann schnell, ob unser Paket mit seinem Status zurückkehren kann:

@Test public void givenDeliveredPackage_whenPrevState_thenStateOrdered() { Package pkg = new Package(); pkg.setState(new DeliveredState()); pkg.previousState(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); }

Überprüfen Sie anschließend, ob sich der Status geändert hat, und sehen Sie, wie die Implementierung der printStatus () -Methode zur Laufzeit ihre Implementierung ändert:

public class StateDemo { public static void main(String[] args) { Package pkg = new Package(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); } }

Dies gibt uns die folgende Ausgabe:

Package ordered, not delivered to the office yet. Package delivered to post office, not received yet. Package was received by client. This package is already received by a client. Package was received by client.

Da wir den Status unseres Kontexts geändert haben, hat sich das Verhalten geändert, aber die Klasse bleibt dieselbe. Neben der API nutzen wir.

Auch der Übergang zwischen den Zuständen hat stattgefunden, unsere Klasse hat ihren Zustand und folglich ihr Verhalten geändert.

6. Nachteile

Der Nachteil des Zustandsmusters ist die Auszahlung bei der Implementierung des Übergangs zwischen den Zuständen. Das macht den Staat hartcodiert, was im Allgemeinen eine schlechte Praxis ist.

But, depending on our needs and requirements, that might or might not be an issue.

7. State vs. Strategy Pattern

Both design patterns are very similar, but their UML diagram is the same, with the idea behind them slightly different.

First, the strategy pattern defines a family of interchangeable algorithms. Generally, they achieve the same goal, but with a different implementation, for example, sorting or rendering algorithms.

In state pattern, the behavior might change completely, based on actual state.

Next, in strategy, the client has to be aware of the possible strategies to use and change them explicitly. Whereas in state pattern, each state is linked to another and create the flow as in Finite State Machine.

8. Conclusion

Das Zustandsentwurfsmuster ist großartig, wenn wir primitive if / else-Anweisungen vermeiden möchten . Stattdessen extrahieren wir die Logik, um Klassen zu trennen, und lassen unser Kontextobjekt das Verhalten an die in der Statusklasse implementierten Methoden delegieren . Außerdem können wir die Übergänge zwischen den Zuständen nutzen, wobei ein Zustand den Zustand des Kontexts ändern kann.

Im Allgemeinen eignet sich dieses Entwurfsmuster hervorragend für relativ einfache Anwendungen. Für einen fortgeschritteneren Ansatz können wir uns jedoch das Tutorial zu Spring's State Machine ansehen.

Wie üblich ist der vollständige Code im GitHub-Projekt verfügbar.