Skip to content

Latest commit

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

State machines

State machines in Java.

Enables both type checked and runtime checked transitions

Typechecked, if we try to transition straight from green to red it will fail to compile

sealedinterfaceTrafficLightextendsState<TrafficLight>
permits Green, SolidAmber, FlashingAmber, Red {}
staticfinalclassGreenimplementsTrafficLight, TransitionTo<SolidAmber> {}
staticfinalclassSolidAmberimplementsTrafficLight, TransitionTo<Red> {}
staticfinalclassRedimplementsTrafficLight, TransitionTo<FlashingAmber> {}
staticfinalclassFlashingAmberimplementsTrafficLight, TransitionTo<Green> {}
@Testpublicvoidtraffic_light_typechecked_example() {
Greensignal = newGreen();
// Comment out a transition and it will fail to compile.signal = signal
.transition(SolidAmber::new)
.transition(Red::new)
.transition(FlashingAmber::new)
.transition(Green::new);
}

We can still have typechecked transitions even where multiple state transitions are possible

staticfinalclassPendingimplementsOrderStatus, BiTransitionTo<CheckingOut, Cancelled> {}
pending.transition(CheckingOut::new); // finepending.transition(Cancelled::new); // finepending.transition(Refunded::new); // Compile Error

When using the runtime checked transitions we'd throw an exception if we can't transition

@Testpublicvoidruntime_transitions_possible() {
TrafficLightlight = newGreen();
light = light
.tryTransition(SolidAmber::new)
.unchecked();
assertTrue(lightinstanceofSolidAmber);
}
@Test(expected = State.InvalidStateTransitionException.class)
publicvoidruntime_transitions_throw_exception_when_not_possible() {
TrafficLightlight = newGreen();
light = light
.tryTransition(Red::new)
.unchecked();
}

We can add behaviour on states so that we can perform an appropriate action for the state we're in

@Testpublicvoidbehaviour_on_states() throwsOhNoes {
Customercustomer = customer("spam@example.com");
OrderStatusstate = newPending();
state.notifyProgress(customer, emailSender);
verifyZeroInteractions(emailSender);
state = state
.tryTransition(CheckingOut::new)
.orElseThrow(OhNoes::new)
.transition(Purchased::new);
state.notifyProgress(customer, emailSender);
verify(emailSender).sendEmail(customer.email(), "Your order is on its way");
verify(emailSender).sendEmail("fulfillment@mycompany.com", "Customer order pending");
}
sealedinterfaceOrderStatusextendsState<OrderStatus>
permits Pending, CheckingOut, Purchased, Shipped, Cancelled, Failed, Refunded {
defaultvoidnotifyProgress(Customercustomer, EmailSendersender) {}
}
staticfinalclassPurchasedimplementsOrderStatus, BiTransitionTo<Shipped, Failed> {
publicvoidnotifyProgress(Customercustomer, EmailSenderemailSender) {
emailSender.sendEmail("fulfillment@mycompany.com", "Customer order pending");
emailSender.sendEmail(customer.email(), "Your order is on its way");
}
}
staticfinalclassCancelledimplementsOrderStatus {
publicvoidnotifyProgress(Customercustomer, EmailSenderemailSender) {
emailSender.sendEmail("fulfillment@mycompany.com", "Customer order cancelled");
emailSender.sendEmail(customer.email(), "Your order has been cancelled");
}
}

We can also have guards before and after transitions. Our states can be non-static classes if we want to access state in the enclosing class, like this logger.

LoggerfailureLog = Logger.getLogger("failures");
finalclassFailedimplementsOrderStatus {
@OverridepublicvoidafterTransition(OrderStatusfrom) {
failureLog.warning("Oh bother! failed from " + from.getClass().getSimpleName());
}
}

About

Typechecked or Runtimechecked State Machines in Java

Resources

Stars

51 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages