This post is about a thought experiment for what it would be like to program a reactive graphical user-interface in Java using the following abstractions:
- Event streams - loosely inspired by Functional Reactive programming.
- Finite-state automata that communicate over the event streams.
An event stream can be either an input event stream or an output event stream. Reactions to events on a stream are implemented with pub/sub i.e. the observer design pattern. An event stream is also an observable. An input event stream can be bound to a new output event stream, by adding an observer to the output stream, which notifies the input stream with streamed values. The class InputEventSream, shown just below, is a subject (observable) capable of receiving events of the generic type I. The bind method creates an output event stream that is capable of sending events of type I to the input stream that receives the bind method invocation. Bind takes a filter that converts the output values of type O to input values of type I. An identity filter can be used when no conversion is required. public class InputEventStream <I> implements Subject<I> { public void addObserver(Observer<I> o); public void notifyObservers(I v); public <O> OutputEventStream<O> bind(final EventStreamFilter<O,I> eventFilter); } Values are thus always sent on output event streams: public class OutputEventStream<O> implements Subject<O> { public void send(O v); }
A state machine is parameterised by the generic types I and O for the types of the input and output values for which the machine is capable of consuming and producing, respectively: class Machine<I, O> { protected InputEventStream<I> inputEventStream protected OutputEventStream<O> outputEventStream; private State<I> state; public void setState(State<I> h) { this.state = h; } public boolean update(I e) { return state.update(e); } } The machine processes an input event with a value of type I by delegating to the class State<I>. The state class processes the event and changes the state of the machine, through invoking the setState method. This is done using deeply nested anonymous inner classes. Thus, any states higher up within the lexical scope can be set as the current state, provided that they are declared with the final keyword-modifier. The machine produces output events of type O on its output event stream. A machine has typical accessors for its event streams, which are not shown here.
I have implemented a simple calculator using these machine and event stream classes. The calculator-machine's generic types I and O are instantiated with InputEnum and BigDecimal, respectively. That is, the calculator machine takes inputs that are digits, 0 to 9, operators ., +, -, /, *, commands = and C (for clear). The InputEnum class is a Java enum to enable pattern matching, Java style. The calculator machine produces BigDecimal output values.
To integrate the state machine into a GUI, its input stream is simply bound to the buttons of the GUI and the output stream is bound to a text field for display. The complete code for the calculator and its supporting classes were coded up very quickly and so is work in progress but are available in my github repo for this post.
The unit test for the calculator machine, below, sends a stream of characters to the machine (converted into InputEnum's) and expects its computed result. (Note this test uses constants for demonstration only.) @Test public void runCalculator() { CalculatorMachine c = new CalculatorMachine(32); // display width 32 chars String input = "12.3+3.5+1.1*2="; for (int i = 0; i < input.length(); ++i) { c.update(InputEnum.map(input.charAt(i))); } Assert.assertEquals("33.8", c.getValue().toString()); }
My conclusions from this little programming experiment are:
- The state machine abstraction supports Test Driven Development with a cleanly defined input and ouput interface. Indeed, machines define their inputs and outputs with formal languages. The troubles of traditional TDD with say, JMock are avoided, e.g. getting tangled up with expectations over the chain of method calls with a test object and its dependencies.
- Compositional reasoning could be applied to integration testing, i.e. I could be convinced that the application works properly without an automated test that starts up the GUI and pushes its buttons. These kinds of tests make the build process longer. With such reasoning, only 2 unit tests are needed: for the machine and for the binding of its streams to the GUI.
- I claim that communicating state-machines scale well with the complexity of GUI programming and offers a uniform model in terms of the MVC framework.