Eintrag
In den Projekten traf ich drei Beispiele, die auf die eine oder andere Weise mit der
Theorie der endlichen Automaten verbunden waren- Beispiel 1. Ein unterhaltsamer
Govnokod- Code . Es braucht viel Zeit, um zu verstehen, was passiert. Ein charakteristisches Merkmal der Ausführungsform der angegebenen Theorie im Code ist ein ziemlich heftiger Dump, der manchmal einem prozeduralen Code stark ähnelt. Die Tatsache, dass diese Version des Codes besser ist, das Projekt nicht zu berühren, kennt jeden Technologen, Methodologen und Produktspezialisten. Sie gehen in diesen Code, um im Notfall etwas zu reparieren (wenn es komplett kaputt ist). Es ist keine Frage, ob Funktionen abgeschlossen werden sollen. Denn es ist beängstigend zu brechen. Das zweite auffällige Merkmal, das diesen Typ isoliert, ist das Vorhandensein derart leistungsfähiger Schalter im Vollbildmodus.
Es gibt sogar einen Witz in dieser Hinsicht:
Optimale GrößeAuf einem der JPoint sprach einer der Sprecher, vielleicht Nikolai Alimenkov, darüber, wie viele Fälle im Switch normal sind, und sagte, die beste Antwort sei "bisher passt in den Bildschirm". Wenn es stört und Ihr Switch bereits nicht normal ist, nehmen Sie die Schriftgröße in der IDE und reduzieren Sie sie
- Beispiel 2. Musterstatus . Die Hauptidee (für diejenigen, die Links nicht folgen möchten) ist, dass wir eine bestimmte Geschäftsaufgabe in eine Reihe von Endzuständen aufteilen und diese mit Code beschreiben.
Der Hauptnachteil von Pattern State ist, dass die Staaten voneinander wissen, dass es Brüder gibt und sich gegenseitig anrufen. Ein solcher Code ist ziemlich schwer universell zu machen. Wenn Sie beispielsweise ein Zahlungssystem mit mehreren Arten von Zahlungen implementieren, besteht die Gefahr, dass Sie sich so sehr mit Generika beschäftigen, dass die Deklaration Ihrer Methoden ungefähr so aussehen kann:
private < T extends BaseContextPayment, Q extends BaseDomainPaymentRequest, S, B extends AbstractPaymentDetailBuilder<T, Q, S, B>, F extends AbstractPaymentBuilder<T, Q, S, B> > PaymentContext<T, S> build(final Q request, final Class<F> factoryClass){
Zusammenfassender Status: Eine Implementierung kann zu ziemlich kompliziertem Code führen. - Beispiel 3 StateMachine Die Hauptidee des Musters ist, dass Zustände nichts voneinander wissen, die Übergangssteuerung durch den Kontext erfolgt, es ist besser, weniger Konnektivität - der Code ist einfacher.
Nachdem wir die gesamte „Kraft“ des ersten Typs und die Komplexität des zweiten Typs erfahren hatten, entschieden wir uns, Pattern StateMachine für den neuen Business Case zu verwenden.
Um das Rad nicht neu zu erfinden, wurde beschlossen, Statemachine Spring als Basis zu verwenden (dies ist Spring).
Nachdem ich die Docks gelesen hatte, ging ich zu YouTube und Habr (um zu verstehen, wie die Leute damit arbeiten, wie es sich auf dem Produkt anfühlt, welche Art von Rechen usw.). Es stellte sich heraus, dass es wenig Informationen gibt, auf YouTube gibt es ein paar Videos, die alle recht oberflächlich sind. Zu Habré zu diesem Thema fand ich nur einen Artikel sowie das Video recht oberflächlich.
In einem Artikel ist es unmöglich, alle Feinheiten der Arbeit von Spring Statemachine zu beschreiben, um das Dock herumzugehen und alle Fälle zu beschreiben, aber ich werde versuchen, das Wichtigste und Gefragteste zu erzählen, und über Rechen, speziell für mich, als ich mit dem Framework vertraut wurde, waren die folgenden Informationen wäre sehr hilfreich.
Hauptteil
Wir erstellen eine Spring Boot-Anwendung und fügen einen Webstarter hinzu (die Webanwendung wird so schnell wie möglich ausgeführt). Die Anwendung ist eine Abstraktion des Kaufprozesses. Das Produkt beim Kauf durchläuft die Phasen des neuen, reservierten, reservierten Rückgangs und des vollständigen Kaufs.
Ein bisschen Improvisation, es würde mehr Status in einem realen Projekt geben, aber na ja, wir haben auch ein sehr reales Projekt.
Fügen Sie in der pom.xml der neu gebackenen Webanwendung die Abhängigkeit vom Computer und den Tests dafür hinzu (Web Starter sollte bereits vorhanden sein, wenn er über
start.spring.io erfasst wird ):
<dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>2.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-test</artifactId> <version>2.1.3.RELEASE</version> <scope>test</scope> </dependency> <cut />
Erstellen Sie die Struktur:

Ich muss noch nicht auf Details dieser Struktur eingehen, ich werde alles nacheinander erklären und am Ende des Artikels wird ein Link zur Quelle angezeigt.
Also lass uns gehen.
Wir haben ein sauberes Projekt mit den notwendigen Abhängigkeiten, zunächst werden wir eine Aufzählung mit Zuständen und Ereignissen erstellen, eine ziemlich einfache Abstraktion, diese Komponenten selbst tragen keine Logik.
public enum PurchaseEvent { RESERVE, BUY, RESERVE_DECLINE }
public enum PurchaseState { NEW, RESERVED, CANCEL_RESERVED, PURCHASE_COMPLETE }
Obwohl formal, können Sie dieser Aufzählung Felder hinzufügen und darin etwas fest codieren, das beispielsweise für einen bestimmten Zustand charakteristisch ist, was ziemlich logisch ist (wir haben dies getan, indem wir unseren Fall ganz bequem gelöst haben).
Wir werden den Computer über die Java-Konfiguration konfigurieren, die Konfigurationsdatei erstellen und für die Erweiterungsklasse EnumStateMachineConfigurerAdapter <PurchaseState, PurchaseEvent>. Da unser Status und Ereignis enum ist, ist die Schnittstelle angemessen, aber es ist nicht erforderlich, dass jeder Objekttyp als generisch verwendet werden kann (andere Beispiele in diesem Artikel werden nicht berücksichtigt, da EnumStateMachineConfigurerAdapter meiner Meinung nach mehr als ausreichend ist).
Der nächste wichtige Punkt ist, ob ein Computer im Anwendungskontext ausgeführt wird: in einer einzelnen Instanz von @EnableStateMachine oder jedes Mal, wenn eine neue @EnableStateMachineFactory erstellt wird. Wenn es sich um eine Mehrbenutzer-Webanwendung mit mehreren Benutzern handelt, ist die erste Option für Sie kaum geeignet. Daher verwenden wir die zweite Option als die beliebteste. StateMachine kann auch über den Builder als reguläre Bean erstellt werden (siehe Dokumentation). Dies ist in einigen Fällen praktisch (z. B. muss die Maschine explizit als Bean deklariert werden). Wenn es sich um eine separate Bean handelt, können wir unseren Umfang angeben z. B. Sitzung oder Anfrage. In unserem Projekt wurde der Wrapper (Funktionen unserer Geschäftslogik) über die Statemachine-Bean implementiert, der Wrapper war Singleton und der Prototyp der Maschine selbst
RechenWie implementiere ich einen Prototyp in Singlton?
Tatsächlich müssen Sie bei jedem Zugriff auf das Objekt lediglich eine neue Bean aus dem applicationContext abrufen. Das Einfügen eines applicationContext in die Geschäftslogik ist eine Sünde. Daher muss eine Bean-Statemachine entweder eine Schnittstelle mit mindestens einer Methode oder eine abstrakte Methode (Methodeninjektion) implementieren. Wenn Sie eine Java-Konfiguration erstellen, müssen Sie die angegebene abstrakte Methode implementieren, und in der Implementierung werden wir darauf zurückgreifen applicationContext neue Bean. Es ist üblich, in der Konfigurationsklasse einen Link zum applicationContext zu haben, und über die abstrakte Methode rufen wir .getBean () auf.
Die EnumStateMachineConfigurerAdapter-Klasse verfügt über mehrere Methoden, die überschreiben, welche wir den Computer konfigurieren.
Registrieren Sie zunächst die Status:
@Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .states(EnumSet.allOf(PurchaseState.class)); }
initial (NEW) ist der Status, in dem sich die Maschine befindet, nachdem die Bean erstellt wurde. end (PURCHASE_COMPLETE) ist der Status, in dem die Maschine statemachine.stop () ausführt, für eine nicht deterministische Maschine (von der die meisten) irrelevant sind, aber etwas muss angegeben werden . .states (EnumSet.allOf (PurchaseState.class) Liste aller Status, die Sie in großen Mengen verschieben können.
Konfigurieren Sie die globalen Maschineneinstellungen
@Override public void configure(final StateMachineConfigurationConfigurer<PurchaseState, PurchaseEvent> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(new PurchaseStateMachineApplicationListener()); }
Hier bestimmt autoStartup, ob der Computer standardmäßig unmittelbar nach der Erstellung gestartet wird, dh ob er automatisch in den Status NEW wechselt (standardmäßig false). Sofort registrieren wir einen Listener für den Computerkontext (etwas später). In derselben Konfiguration können Sie einen separaten TaskExecutor festlegen. Dies ist praktisch, wenn für einige Übergänge eine lange Aktion ausgeführt wird und die Anwendung weiter gehen sollte.
Nun, die Übergänge selbst:
@Override public void configure(final StateMachineTransitionConfigurer<PurchaseState, PurchaseEvent> transitions) throws Exception { transitions .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(CANCEL_RESERVED) .event(RESERVE_DECLINE) .action(cancelAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .action(buyAction(), errorAction()); }
Die gesamte Logik von Übergängen oder Übergängen wird hier festgelegt. Guard kann an Übergänge gehängt werden, eine Komponente, die immer Boolesche Werte zurückgibt. Was genau werden Sie nach eigenem Ermessen beim Übergang von einem Status in einen anderen überprüfen? Jede Logik kann in Guard perfekt sein. Dies ist eine ganz normale Komponente aber er muss boolesch zurückkehren. Im Rahmen unseres Projekts kann HideGuard beispielsweise eine bestimmte Einstellung überprüfen, die der Benutzer festlegen konnte (dieses Produkt nicht anzeigen), und die Maschine entsprechend nicht in den von Guard geschützten Zustand versetzen. Ich stelle fest, dass Guard, nur einer kann zu einem Übergang in der Konfiguration hinzugefügt werden, ein solches Design wird nicht funktionieren:
.withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .guard(veryHideGuard())
Genauer gesagt wird es funktionieren, aber nur der erste Wächter (hideGuard ())
Sie können jedoch mehrere Aktionen hinzufügen (jetzt geht es um Aktionen, die wir in der Übergangskonfiguration vorschreiben). Ich persönlich habe versucht, einem Übergang drei Aktionen hinzuzufügen.
.withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction())
Das zweite Argument ist ErrorAction. Die Steuerung wird darauf zugreifen, wenn die ReservedAction eine Ausnahme auslöst (throw ).
RechenDenken Sie daran, dass Sie, wenn Sie den Fehler in Ihrer Aktion weiterhin über try / catch behandeln, nicht in ErrorAction wechseln. Wenn Sie ErrorAction verarbeiten und aufrufen müssen, sollten Sie beispielsweise RuntimeException () aus catch werfen (Sie selbst sagten, dass es sehr notwendig ist).
Zusätzlich zum "Hängen" von Aktionen in Übergängen können Sie sie auch in der Konfigurationsmethode für den Status in ungefähr der folgenden Form "hängen":
@Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .stateEntry() .stateExit() .state() .states(EnumSet.allOf(PurchaseState.class)); }
Es hängt alles davon ab, wie Sie die Aktion ausführen möchten.
RechenBeachten Sie Folgendes, wenn Sie beim Konfigurieren von state () eine Aktion angeben
states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .state(randomAction())
Es wird asynchron ausgeführt. Wenn Sie beispielsweise .stateEntry () sagen, sollte die Aktion direkt am Eingang ausgeführt werden. Wenn Sie jedoch .state () sagen, sollte die Aktion im Zielstatus ausgeführt werden, aber es ist nicht so wichtig, wann.
In unserem Projekt haben wir alle Aktionen in der Übergangskonfiguration konfiguriert, da Sie sie mehrere gleichzeitig aufhängen können.
Die endgültige Version der Konfiguration sieht folgendermaßen aus:
@Configuration @EnableStateMachineFactory public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<PurchaseState, PurchaseEvent> { @Override public void configure(final StateMachineConfigurationConfigurer<PurchaseState, PurchaseEvent> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(new PurchaseStateMachineApplicationListener()); } @Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .stateEntry() .stateExit() .state() .states(EnumSet.allOf(PurchaseState.class)); } @Override public void configure(final StateMachineTransitionConfigurer<PurchaseState, PurchaseEvent> transitions) throws Exception { transitions .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(CANCEL_RESERVED) .event(RESERVE_DECLINE) .action(cancelAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .action(buyAction(), errorAction()); } @Bean public Action<PurchaseState, PurchaseEvent> reservedAction() { return new ReservedAction(); } @Bean public Action<PurchaseState, PurchaseEvent> cancelAction() { return new CancelAction(); } @Bean public Action<PurchaseState, PurchaseEvent> buyAction() { return new BuyAction(); } @Bean public Action<PurchaseState, PurchaseEvent> errorAction() { return new ErrorAction(); } @Bean public Guard<PurchaseState, PurchaseEvent> hideGuard() { return new HideGuard(); } @Bean public StateMachinePersister<PurchaseState, PurchaseEvent, String> persister() { return new DefaultStateMachinePersister<>(new PurchaseStateMachinePersister()); }
Achten Sie auf das Schema der Maschine, es ist sehr deutlich sichtbar, was genau wir codiert haben (welche Übergänge bei welchen Ereignissen gültig sind, welcher Guard den Status schützt und was getan wird, wenn der Status gewechselt wird, welche Aktion).

Machen wir den Controller:
@RestController @SuppressWarnings("unused") public class PurchaseController { private final PurchaseService purchaseService; public PurchaseController(PurchaseService purchaseService) { this.purchaseService = purchaseService; } @RequestMapping(path = "/reserve") public boolean reserve(final String userId, final String productId) { return purchaseService.reserved(userId, productId); } @RequestMapping(path = "/cancel") public boolean cancelReserve(final String userId) { return purchaseService.cancelReserve(userId); } @RequestMapping(path = "/buy") public boolean buyReserve(final String userId) { return purchaseService.buy(userId); } }
Serviceschnittstelle
public interface PurchaseService { boolean reserved(String userId, String productId); boolean cancelReserve(String userId); boolean buy(String userId); }
RechenWissen Sie, warum es wichtig ist, Bean über die Benutzeroberfläche zu erstellen, wenn Sie mit Spring arbeiten? Konfrontiert mit diesem Problem (na ja, ja, und Zhenya Borisov sprach im Ripper), als sie einmal in der Steuerung versuchten, eine improvisierte, nicht leere Schnittstelle zu implementieren. Spring erstellt einen Proxy für Komponenten. Wenn die Komponente keine Schnittstelle implementiert, erfolgt dies über CGLIB. Sobald Sie jedoch eine Schnittstelle implementieren, versucht Spring, einen Proxy über einen dynamischen Proxy zu erstellen. Als Ergebnis erhalten Sie einen unverständlichen Objekttyp und eine NoSuchBeanDefinitionException .
Der nächste wichtige Punkt ist, wie Sie den Status Ihres Computers wiederherstellen, da für jeden Aufruf eine neue Bean erstellt wird, die nichts über Ihren vorherigen Status des Computers und dessen Kontext weiß.
Für diese Zwecke verfügt die Feder-Statemaschine über einen Persistens-Mechanismus:
public class PurchaseStateMachinePersister implements StateMachinePersist<PurchaseState, PurchaseEvent, String> { private final HashMap<String, StateMachineContext<PurchaseState, PurchaseEvent>> contexts = new HashMap<>(); @Override public void write(final StateMachineContext<PurchaseState, PurchaseEvent> context, String contextObj) { contexts.put(contextObj, context); } @Override public StateMachineContext<PurchaseState, PurchaseEvent> read(final String contextObj) { return contexts.get(contextObj); } }
Für unsere naive Implementierung verwenden wir die übliche Map als Statusspeicher. In einer nicht naiven Implementierung handelt es sich um eine Art Datenbank. Achten Sie auf den dritten generischen Typ String. Dies ist der Schlüssel, mit dem der Status Ihres Computers mit allen Status, Variablen im Kontext und der ID gespeichert wird usw. In meinem Beispiel habe ich die Benutzer-ID für den Speicherschlüssel verwendet. Dies kann ein beliebiger Schlüssel sein (Benutzer session_id, eindeutige Anmeldung usw.).
RechenIn unserem Projekt passte der Mechanismus zum Speichern und Wiederherstellen von Status aus der Box nicht zu uns, da wir den Status der Maschine in der Datenbank gespeichert hatten und durch einen Job geändert werden konnten, der nichts über die Maschine wusste.
Ich musste den von der Datenbank empfangenen Status festigen, eine InitAction ausführen, die beim Starten des Computers den Status von der Datenbank erhielt, ihn zwangsweise festlegte und erst dann ein Ereignis auslöste, ein Beispiel für Code, der die oben genannten Schritte ausführt:
stateMachine .getStateMachineAccessor() .doWithAllRegions(access -> { access.resetStateMachine(new DefaultStateMachineContext<>({ResetState}, null, null, null, null)); }); stateMachine.start(); stateMachine.sendEvent({NewEventFromResetState});
Wir werden die Implementierung des Dienstes in jeder Methode betrachten:
@Override public boolean reserved(final String userId, final String productId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); stateMachine.getExtendedState().getVariables().put("PRODUCT_ID", productId); stateMachine.sendEvent(RESERVE); try { persister.persist(stateMachine, userId); } catch (final Exception e) { e.printStackTrace(); return false; } return true; }
Wir holen das Auto ab Werk, setzen einen Parameter in den Maschinenkontext, in unserem Fall handelt es sich um eine Produkt-ID. Der Kontext ist eine Art Box, in die Sie alles, was Sie brauchen, einfügen können, wo immer Zugriff auf die Statemachine Bean oder ihren Kontext besteht, da die Maschine automatisch startet, wenn der Kontext startet , dann nach dem Start, unser Auto wird in den Status NEU, werfen Sie die Veranstaltung auf die Reservierung von Waren.
Die verbleibenden zwei Methoden sind ähnlich:
@Override public boolean cancelReserve(final String userId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); try { persister.restore(stateMachine, userId); stateMachine.sendEvent(RESERVE_DECLINE); } catch (Exception e) { e.printStackTrace(); return false; } return true; } @Override public boolean buy(final String userId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); try { persister.restore(stateMachine, userId); stateMachine.sendEvent(BUY); } catch (Exception e) { e.printStackTrace(); return false; } return true; }
Hier stellen wir zuerst den Status der Maschine für die Benutzer-ID eines bestimmten Benutzers wieder her und lösen dann ein Ereignis aus, das der API-Methode entspricht.
Beachten Sie, dass productId nicht mehr in der Methode angezeigt wird. Wir haben sie dem Computerkontext hinzugefügt und erhalten sie nach dem Wiederherstellen des Computers aus der Sicherung.
In der Aktionsimplementierung erhalten wir die Produkt-ID aus dem Maschinenkontext und zeigen eine Meldung an, die dem Übergang im Protokoll entspricht. Ich gebe beispielsweise den Code ReservedAction:
public class ReservedAction implements Action<PurchaseState, PurchaseEvent> { @Override public void execute(StateContext<PurchaseState, PurchaseEvent> context) { final String productId = context.getExtendedState().get("PRODUCT_ID", String.class); System.out.println(" " + productId + " ."); } }
Wir können nur den Listener erwähnen, der sofort einige Skripte bietet, an denen Sie sich festhalten können. Überzeugen Sie sich selbst:
public class PurchaseStateMachineApplicationListener implements StateMachineListener<PurchaseState, PurchaseEvent> { @Override public void stateChanged(State<PurchaseState, PurchaseEvent> from, State<PurchaseState, PurchaseEvent> to) { if (from.getId() != null) { System.out.println(" " + from.getId() + " " + to.getId()); } } @Override public void stateEntered(State<PurchaseState, PurchaseEvent> state) { } @Override public void stateExited(State<PurchaseState, PurchaseEvent> state) { } @Override public void eventNotAccepted(Message<PurchaseEvent> event) { System.out.println(" " + event); } @Override public void transition(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void transitionStarted(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void transitionEnded(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void stateMachineStarted(StateMachine<PurchaseState, PurchaseEvent> stateMachine) { System.out.println("Machine started"); } @Override public void stateMachineStopped(StateMachine<PurchaseState, PurchaseEvent> stateMachine) { } @Override public void stateMachineError(StateMachine<PurchaseState, PurchaseEvent> stateMachine, Exception exception) { } @Override public void extendedStateChanged(Object key, Object value) { } @Override public void stateContext(StateContext<PurchaseState, PurchaseEvent> stateContext) { } }
Das einzige Problem ist, dass dies eine Schnittstelle ist, was bedeutet, dass Sie alle diese Methoden implementieren müssen. Da es jedoch unwahrscheinlich ist, dass Sie alle benötigen, bleiben einige von ihnen leer. Diese Abdeckung besagt, dass die Methoden nicht durch Tests abgedeckt sind.
Hier in lisener können wir absolut alle Metriken an völlig unterschiedliche Ereignisse der Maschine hängen (zum Beispiel gehen Zahlungen nicht durch, die Maschine geht oft in eine Art PAYMENT_FAIL-Status über, wir hören Übergänge und wenn die Maschine in einen fehlerhaften Status übergeht - wir schreiben in ein ungerades Protokoll, oder Basis oder rufen Sie die Polizei, was auch immer).
RechenEs gibt ein Ereignis stateMachineError in lisener-e, aber mit einer Nuance, wenn Sie eine Ausnahme haben und diese in catch behandeln, berücksichtigt die Maschine nicht, dass ein Fehler aufgetreten ist. Sie müssen explizit in catch sprechen
stateMachine.setStateMachineError (Ausnahme) und übergeben Sie einen Fehler.
Um zu überprüfen, was wir getan haben, werden wir zwei Fälle ausführen:
- 1. Reservierung und anschließende Ablehnung des Kaufs. Wir senden der Anwendung eine Anfrage für den URI "/ Reserve" mit den Parametern userId = 007, productId = 10001 und danach die Anfrage "/ cancel" mit dem Parameter userId = 007. Die Konsolenausgabe lautet wie folgt:
Machine started
10001 .
NEW RESERVED
Machine started
10001
RESERVED CANCEL_RESERVED
- 2. Reservierung und erfolgreicher Kauf:
Machine started
10001 .
NEW RESERVED
Machine started
10001
RESERVED PURCHASE_COMPLETE
Fazit
Abschließend werde ich ein Beispiel für das Testen des Frameworks geben. Ich denke, aus dem Code wird alles klar. Sie benötigen lediglich eine Abhängigkeit von der Testmaschine und können die Konfiguration deklarativ überprüfen.
@Test public void testWhenReservedCancel() throws Exception { StateMachine<PurchaseState, PurchaseEvent> machine = factory.getStateMachine(); StateMachineTestPlan<PurchaseState, PurchaseEvent> plan = StateMachineTestPlanBuilder.<PurchaseState, PurchaseEvent>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates(NEW) .expectStateChanged(0) .and() .step() .sendEvent(RESERVE) .expectState(RESERVED) .expectStateChanged(1) .and() .step() .sendEvent(RESERVE_DECLINE) .expectState(CANCEL_RESERVED) .expectStateChanged(1) .and() .build(); plan.test(); } @Test public void testWhenPurchaseComplete() throws Exception { StateMachine<PurchaseState, PurchaseEvent> machine = factory.getStateMachine(); StateMachineTestPlan<PurchaseState, PurchaseEvent> plan = StateMachineTestPlanBuilder.<PurchaseState, PurchaseEvent>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates(NEW) .expectStateChanged(0) .and() .step() .sendEvent(RESERVE) .expectState(RESERVED) .expectStateChanged(1) .and() .step() .sendEvent(BUY) .expectState(PURCHASE_COMPLETE) .expectStateChanged(1) .and() .build(); plan.test(); }
RechenWenn Sie Ihre Maschine plötzlich testen möchten, ohne den Kontext mit herkömmlichen Komponententests zu erhöhen, können Sie eine Maschine über den Builder erstellen (teilweise oben erläutert), eine Klasseninstanz mit einer Konfiguration erstellen und von dort aus Action und Guard erhalten. Sie funktioniert ohne Kontext. Sie können einen kleinen Test schreiben Das Framework ist verspottet. Es ist ein Plus, zu überprüfen, welche Aktionen aufgerufen wurden, welche nicht, wie oft usw. in verschiedenen Fällen.
PS
Unser Auto arbeitet produktiv. Bisher sind keine Betriebsprobleme aufgetreten. Es wird eine Funktion eingeführt, mit der wir die überwiegende Mehrheit der Komponenten der aktuellen Maschine bei der Implementierung einer neuen Maschine verwenden können (Guard und einige Aktionen sind einfach perfekt).Hinweis
Ich habe es in dem Artikel nicht berücksichtigt, aber ich möchte solche Möglichkeiten wie Auswahl erwähnen. Dies ist eine Art Auslöser, der nach dem Schalterprinzip funktioniert, bei dem Wachen an Fällen aufgehängt sind und die Maschine abwechselnd versucht, in den Zustand zu wechseln, der in der Auswahlkonfiguration beschrieben ist und in dem Wache ihn loslässt. Ohne einige Ereignisse ist es praktisch, wenn wir beim Initialisieren des Computers automatisch zu einer Art Pseudo-Host wechseln müssen.Referenzen
DocaQuellcode