Keller State with a State Context - ThorbenKuck/Keller GitHub Wiki
Concept
After seeing the example without a State Context, you might think, that the StateMachine in itself is a StateContext and in a sense, this is true. The StateMachine holds refferences to the Current State. But in most cases, the StatePattern is used, to modify some context other than a framework class.
For that, you can provide an existing StateContext to the StateMachine, which provides a method to update the internally maintained State of your context.
With this approach you are getting close to a real State Pattern. The role of the StateMachine in this way is only a delegator. It finds and delegates new States, StateTransitions and so on.
Using Keller State with a State Context
For explaining this, i want to convert the Wikipedia State Pattern example in Java. In our example, we have an existing Business Logic. I changed some basic things, like lower case the parameters and so on.
Step 0, The Existing Business Logic
Our Business Logic looks like this. First off, we have the interface, which is a bit changed.
interface Statelike {
void writeName(final String name);
}
Since in our example, we want to convert existing business logic into the state pattern, we don't know about the State Context. Our 2 States look the following.
class StateA implements Statelike {
@Override
public void writeName(final String name) {
System.out.println(name.toLowerCase());
}
}
class StateB implements Statelike {
/** State counter */
private int count = 0;
@Override
public void writeName(final String name) {
System.out.println(name.toUpperCase());
++count;
}
}
Those 2 States are a bit arbitrary, but they are just for an example.
This is our existing Business Logic that we want to convert into a State Pattern with a new State Context. So we Code our StateContext.
public class StateContext {
private Statelike myState;
public void setState(final Statelike newState) {
myState = newState;
}
public void writeName(final String name) {
myState.writeName(name);
}
}
Step 1, Introducing the StateContext
In our Main class (or somewhere else), we have our StateMachine. All we do is, that we provide the StateMachine with the first State and the State Context. The StateContext will be given to whatever want's to use it. In the Wikipedia example, this was the main method, in our example, it is an imaginary method called run.
class Main {
public static void main(String[] args) {
final StateContext stateContext = new StateContext();
final StateMachine stateMachine = StateMachine.create();
stateMachine.setStateContext(stateContext);
stateMachine.start(new StateA());
handle(stateContext);
}
}
the method run does the following
public static void handle(StateContext stateContext) {
stateContext.writeName("Montag");
stateContext.writeName("Dienstag");
stateContext.writeName("Mittwoch");
stateContext.writeName("Donnerstag");
stateContext.writeName("Freitag");
stateContext.writeName("Samstag");
stateContext.writeName("Sonntag");
}
Our StateMachine is ready for use, but our StateContext is not quit ready. For the StateContext, we must provide a way of setting the next State. We do that, by providing the @InjectState onto the Setter-Method, that sets the State
public class StateContext {
private Statelike myState;
@InjectState
public void setState(final Statelike newState) {
myState = newState;
}
public void writeName(final String name) {
myState.writeName(name);
}
}
This actually does work now, but we only provided a single state and no StateTransition. So internally, the first state is set (StateA), no following state is provided so the StateMachine is finished and the output will be the following:
montag
dienstag
mittwoch
donnerstag
freitag
samstag
sonntag
all lower case.
Step 2, State to State
For this Step, we need 2 Things. The NextState and the StateTransitionFactory. Because of the nature, that the Wikipedia example is structured, every State defines its following state. But the StateMachine will (in our example) define when the States produce the next States.
Let's first provide the @NextState methods.
public class StateB implements Statelike {
/**
* State counter
*/
private int count = 0;
@Override
public void writeName(String name) {
System.out.println(name.toUpperCase());
++count;
}
@NextState
public Statelike nextState() {
if (++count > 1) {
return new StateA();
}
return this;
}
}
class StateA implements Statelike {
@Override
public void writeName(final String name) {
System.out.println(name.toLowerCase());
}
@NextState
public Statelike nextState() {
return new StateB();
}
}
This has the same effect as in the Wikipedia example. However, running the programm will lead to the following output:
None, sorry, it is none. Why? Well, the programm does not terminate either. In Fact, the States will be added one after another again and again and again. Always in the same Pattern StateA -> StateB -> StateB -> StateA ...
For that, we now do 2 more Things.
First, we provide a state transition. In this Example, we will do this in the State itself (to stay true to the example we are adopting), but you could also do it through the StateContext. But this will (in this case) be the worse option.
We want to continue to the next State, if:
(in StateA) writeName was called.
(in StateB) counter > 1.
public class StateB implements Statelike {
/**
* State counter
*/
private int count = 0;
private final StateTransition stateTransition = StateTransition.open();
@Override
public void writeName(String name) {
System.out.println(name.toUpperCase());
if(++count > 1) {
stateTransition.finish();
}
}
@StateTransitionFactory
public StateTransition getStateTransition() {
return stateTransition;
}
@NextState
public Statelike nextState() {
return new StateA();
}
}
class StateA implements Statelike {
private final StateTransition stateTransition = StateTransition.open();
@Override
public void writeName(final String name) {
System.out.println(name.toLowerCase());
stateTransition.finish();
}
@StateTransitionFactory
public StateTransition getStateTransition() {
return stateTransition;
}
@NextState
public Statelike nextState() {
return new StateB();
}
}
Running this Example will print out something. The output will most likely be:
montag
dienstag
mittwoch
donnerstag
freitag
samstag
sonntag
But the programm will not exit. Why is that?
By calling StateMachine#parallel(Object)
, the whole StateMachine handling will be extracted into a seperate Thread. This will not be absolutely random, the method parallel will block until the initial state is set and the initial dependencies are resolved. However, because our main Thread just needs to call StateContext#writeName(String)
and the keller-state-worker Thread needs to resolve the pre analyzed data-sets, the main Thread will most likely be much faster than the keller-state-worker.
Now, this is a big drawback. There is no way around it. But some times, introducing some annotations is the only (fast enough) way of rewriting you application.
Step 3, Snychronization
For that, let's synchronize the state. The annotation @TearDown will be called, after the new state has been created but before the StateAction will be tried to be executed. This will come in handy for us. We introduce a new Synchronize instance for StateA and StatB, that will block right at the end of writeName and that will be released on tearDown. It looks like this
public class StateA implements Statelike {
private final StateTransition stateTransition = StateTransition.open();
private final Synchronize synchronize = Synchronize.ofCountDownLatch();
@Override
public void writeName(String name) {
System.out.println(name.toLowerCase());
stateTransition.finish();
try {
synchronize.synchronize();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@StateTransitionFactory
public StateTransition getStateTransition() {
return stateTransition;
}
@NextState
public Statelike nextState() {
return new StateB();
}
@TearDown
public void tearDown() {
synchronize.goOn();
}
}
public class StateB implements Statelike {
/**
* State counter
*/
private int count = 0;
private final StateTransition stateTransition = StateTransition.open();
private final Synchronize synchronize = Synchronize.ofCountDownLatch();
@Override
public void writeName(String name) {
System.out.println(name.toUpperCase());
if(++count > 1) {
stateTransition.finish();
try {
synchronize.synchronize();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@StateTransitionFactory
public StateTransition getStateTransition() {
return stateTransition;
}
@NextState
public Statelike nextState() {
return new StateA();
}
@TearDown
public void tearDown() {
synchronize.goOn();
}
}
So, after the StateTransition#finish method is called, we release the main Thread, which will block untill the keller-state-worker Thread will have created and injected the next State. This is the output:
montag
DIENSTAG
MITTWOCH
donnerstag
FREITAG
SAMSTAG
sonntag
Step 4, Finishing Touch
For the last Step, let's acutally stop the StateMachine handly. We could return null in a state as NextState, which will stop the StateMachine. But we want to stop it, outside of the States. Simply, let's manipulate the main method
class Main {
public static void main(String[] args) {
final StateContext stateContext = new StateContext();
final StateMachine stateMachine = StateMachine.create();
stateMachine.setStateContext(stateContext);
stateMachine.start(new StateA());
handle(stateContext);
stateMachine.stop();
}
}
Takeaway
Now, this might all appear to be more complicated than it had to be and it realy is. But ofcourse, this is only a basic and verry simple example. If you realy need to, you can convert you business logic into an StateMachine with a custom StateContext. But to be honest, think about whether it is worth it or not. I believe, some if not most cases you will be able to use an easyier ways of rewriting you application to fit the new need.