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