Statligt designmönster i Java

1. Översikt

I denna handledning introducerar vi ett av de beteendemässiga GoF-designmönstren - tillståndsmönstret.

Först ger vi en översikt över dess syfte och förklarar problemet det försöker lösa. Sedan tar vi en titt på statens UML-diagram och implementering av det praktiska exemplet.

2. Statligt designmönster

Huvudidén med statligt mönster är att låta objektet ändra sitt beteende utan att ändra klass. Dessutom, genom att implementera den, bör koden förbli renare utan många if / else-uttalanden.

Tänk dig att vi har ett paket som skickas till ett postkontor, själva paketet kan beställas, sedan levereras till ett postkontor och slutligen tas emot av en klient. Beroende på det aktuella tillståndet vill vi nu skriva ut leveransstatusen.

Det enklaste tillvägagångssättet är att lägga till några booleska flaggor och tillämpa enkla if / else-uttalanden inom var och en av våra metoder i klassen. Det kommer inte att komplicera det mycket i ett enkelt scenario. Det kan dock komplicera och förorena vår kod när vi får fler tillstånd att bearbeta vilket kommer att resultera i ännu fler uttalanden om / annat.

Dessutom skulle all logik för vart och ett av staterna spridas över alla metoder. Det är här statsmönstret kan anses använda. Tack vare det statliga designmönstret kan vi inkapsla logiken i dedikerade klasser, tillämpa principen om enskilt ansvar och öppna / stängda principer, ha renare och mer underhållbar kod.

3. UML-diagram

I UML-diagrammet ser vi att Context- klassen har ett associerat tillstånd som kommer att ändras under programkörningen.

Vårt sammanhang kommer att delegera beteendet till statens genomförande. Med andra ord kommer alla inkommande förfrågningar att hanteras av det konkreta genomförandet av staten.

Vi ser att logiken är åtskild och att lägga till nya stater är enkelt - det handlar om att lägga till ytterligare en statlig implementering om det behövs.

4. Implementering

Låt oss utforma vår applikation. Som redan nämnts kan paketet beställas, levereras och tas emot, därför kommer vi att ha tre stater och kontextklassen.

Låt oss först definiera vårt sammanhang, det kommer att bli en paketklass :

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(); } }

Som vi kan se innehåller den en referens för att hantera tillståndet, märka tidigareState (), nextState () och printStatus () -metoder där vi delegerar jobbet till tillståndsobjektet. Staterna kommer att vara länkade till varandra och varje stat kommer att ställa in ytterligare en baserat på denna referens som skickas till båda metoderna.

Klienten kommer att interagera med paketklassen , men han behöver inte ta itu med att ställa in tillstånden, allt klienten behöver göra är att gå till nästa eller föregående tillstånd.

Därefter kommer vi att ha PackageState som har tre metoder med följande signaturer:

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

Detta gränssnitt kommer att implementeras av varje konkret tillståndsklass.

Det första konkreta tillståndet kommer att beställas :

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."); } }

Här pekar vi på nästa tillstånd som kommer att inträffa efter att paketet har beställts. Det ordnade tillståndet är vårt rottillstånd och vi markerar det uttryckligen. Vi kan se i båda metoderna hur övergången mellan stater hanteras.

Låt oss ta en titt på klassen DeliveredState :

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."); } }

Återigen ser vi kopplingen mellan staterna. Paketet ändrar tillstånd från beställt till levererat, meddelandet i printStatus () ändras också.

Den senaste statusen är ReceiverState :

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()); } }

Det är här vi når det sista tillståndet, vi kan bara återgå till det tidigare tillståndet.

Vi ser redan att det finns en viss utdelning eftersom den ena staten känner till den andra. Vi gör dem tätt kopplade.

5. Testning

Låt oss se hur implementeringen beter sig. Låt oss först kontrollera om installationsövergångar fungerar som förväntat:

@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)); }

Kontrollera sedan snabbt om vårt paket kan flytta tillbaka med dess tillstånd:

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

Efter det, låt oss verifiera ändra tillståndet och se hur implementeringen av printStatus () -metoden ändrar dess implementering vid körning:

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(); } }

Detta ger oss följande resultat:

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.

Eftersom vi har förändrat tillståndet i vårt sammanhang förändrades beteendet men klassen förblir densamma. Förutom det API vi använder.

Övergången mellan staterna har också inträffat, vår klass ändrade sitt tillstånd och följaktligen dess beteende.

6. Nackdelar

Nackdel med statligt mönster är utdelningen vid övergång mellan staterna. Det gör staten hårdkodad, vilket är en dålig praxis i allmänhet.

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

Statligt designmönster är bra när vi vill undvika primitiva if / else-uttalanden . Istället extraherar vi logiken för att separera klasser och låter vårt kontextobjekt delegera beteendet till metoderna som implementeras i tillståndsklassen. Dessutom kan vi utnyttja övergångarna mellan staterna, där en stat kan ändra kontextens tillstånd.

I allmänhet är detta designmönster perfekt för relativt enkla applikationer, men för ett mer avancerat tillvägagångssätt kan vi ta en titt på Spring's State Machine tutorial.

Som vanligt är den fullständiga koden tillgänglig i GitHub-projektet.