State Design Pattern in Java

Zustandsentwurfsmuster in Java

1. Überblick

In diesem Lernprogramm stellen wir eines der Verhaltensmuster des GoF-Designs vor - das Statusmuster.

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

2. Zustandsentwurfsmuster

Die Hauptidee des Zustandsmusters istallow the object for changing its behavior without changing its class. Auch durch seine Implementierung sollte der Code ohne viele if / else-Anweisungen sauberer bleiben.

Stellen Sie sich vor, wir haben ein Paket, das an ein Postamt gesendet wird. Das Paket selbst kann bestellt, dann an ein Postamt 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. An dieser Stelle könnte das Zustandsmuster in Betracht gezogen werden. Dank des State-Entwurfsmusters können wir die Logik in dedizierten Klassen kapseln, dieSingle Responsibility Principle- undOpen/Closed Principle, -Rasierreiniger anwenden und den Code besser warten.

3. UML-Diagramm

UML diagram of state design pattern

Im UML-Diagramm sehen wir, dass der KlasseContexteineStatezugeordnet ist, die sich während der Programmausführung ändern wird.

Unser Kontext geht zudelegate the behavior to the state implementation. Mit anderen Worten, alle eingehenden Anfragen werden von der konkreten Implementierung 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 weitereState-Implementierung hinzuzufügen.

4. Implementierung

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

Definieren wir zunächst unseren Kontext, der einePackage-Klasse 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 vonpreviousState(), nextState() and printStatus(), mit denen wir den Job an das Statusobjekt delegieren. Die Zustände werden miteinander verknüpft undevery state will set another one based on this reference an beide Methoden übergeben.

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

Als nächstes erhalten wirPackageState mit drei Methoden mit den folgenden Signaturen:

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 istOrderedState:

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 Status, der nach der Bestellung des Pakets auftritt. Der bestellte Zustand ist unser Stammzustand und wir kennzeichnen ihn ausdrücklich. Wir können in beiden Methoden sehen, wie der Übergang zwischen Zuständen behandelt wird.

Werfen wir einen Blick auf die KlasseDeliveredState:

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, und die Nachricht inprintStatus()ändert sich ebenfalls.

Der letzte Status istReceivedState:

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 Status, wir können nur zum vorherigen Status zurückkehren.

Wir sehen bereits, dass sich etwas auszahlt, 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 Ihr Paket in seinem Status zurückversetzt werden 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 derprintStatus()-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 Staaten hat stattgefunden, unsere Klasse hat ihren Zustand und folglich ihr Verhalten geändert.

6. Nachteile

Der Nachteil des Zustandsmusters ist die Auszahlung beim Implementieren des Übergangs zwischen den Zuständen. Das macht den Zustand hartcodiert, was im Allgemeinen eine schlechte Praxis ist.

Aber je nach unseren Bedürfnissen und Anforderungen kann dies ein Problem sein oder auch nicht.

7. Bundesland vs. Strategiemuster

Beide Entwurfsmuster sind sehr ähnlich, aber ihr UML-Diagramm ist das gleiche, mit der Idee dahinter, dass sie sich leicht unterscheiden.

Erstens diestrategy pattern defines a family of interchangeable algorithms. Im Allgemeinen erreichen sie dasselbe Ziel, jedoch mit einer anderen Implementierung, z. B. Sortieren oder Rendern von Algorithmen.

In state pattern, the behavior might change completely, basierend auf dem tatsächlichen Zustand.

Als nächstesin strategy, the client has to be aware of the possible strategies to use and change them explicitly. Während im Zustandsmuster jeder Zustand mit einem anderen verknüpft ist und den Fluss wie in der Finite State Machine erzeugt.

8. Fazit

Das Zustandsentwurfsmuster ist großartig, wenn wiravoid primitive if/else statements wollen. Stattdessenextract the logic to separate classes und lassen unserecontext object delegate the behavior zu den in der Zustandsklasse implementierten Methoden. Außerdem können wir die Übergänge zwischen den Zuständen nutzen, wobei ein Zustand den Zustand des Kontexts verändern kann.

Im Allgemeinen eignet sich dieses Entwurfsmuster hervorragend für relativ einfache Anwendungen, aber für einen fortgeschritteneren Ansatz können wir unsSpring’s State Machine tutorial ansehen.

Wie üblich ist der vollständige Code fürthe GitHub project verfügbar.