Enforcing State Transitions

To access some service we are using a third-party library with an interface that looks like this:

public class Session {
    public void connect();
    public Answer ask(Question question);
    public void disconnect();
 }

The contract specified in the documentation specifies that:

  1. The first method invoked on the object may only be connect.

  2. The connect method may be used only once on any given instance
  3. On an instance where connect() has been invoked successfully, you must eventually invoke disconnect
  4. No method may be invoked on an instance on which disconnect has been called.

The third one is the trickiest. We always remember to connect. We always forget to disconnect when we’re done.

We can translate these requirements into a state-transition diagram: You have three states that I’ll call PROTO, CONNECTED, DISCONNECTED. The initial state is PROTO. Method connect does a transition from PROTO to CONNECTED, ask from CONNECTED to CONNECTED and disconnect transitions from CONNECTED to DISCONNECTED. There are no other transitions.

Level Seven

“It’s the library’s job to check the contract it is imposing on us”.

Session s = new Session();
s.connect();
// two hundred lines of code using s
s.disconnect();
// some more code massaging the result

So many things can go wrong here. People may add an early “return” in the omitted block at line three, or code may throw an exception in there, preventing the session from being disconnected, or a future maintainer may insert an extra ask invocation in “code massaging the result”, without noticing the disconnect higher up.

If you’re lucky the library will throw an IllegalStateException for the latter, meaning you have Level Six protection for requirement number four. If you are less lucky and they assumed your code obeys the contract, you’ll just get weird behaviour instead. → Level Seven.
For the former mistake (early return) there will most probably be no errors or warnings. You are just leaking resources especially if your application is long-running, like a server.

Level Six

If we are unsure the library includes checks that state transitions are properly followed you may add a wrapper doing that checking:

public class SessionWrapper {
    private enum State { PROTO, CONNECTED, DISCONNECTED }
    private State state = PROTO;
    private Session wrapped = new Session();

    public void connect() {
        switch (state) {
        case PROTO:
            state=CONNECTED;
            wrapped.connect();
            return;
        default:
            throw new IllegalStateException(state.name());
        }
    }

    public Answer ask(Question question) {
        switch (state) {
        case CONNECTED:
            // stay in state CONNECTED
            return wrapped.ask(question);
        default:
            throw new IllegalStateException(state.name());
        }
    }

    public void disconnect() {
        switch (state) {
        case CONNECTED:
            state=DISCONNECTED;
            wrapped.disconnect();
            return;
        default:
            throw new IllegalStateException(state.name());
        }
    }
}

Some notes:

  • I used switch statements to make it explicit how any contract that can be expressed as a deterministic state-transition diagram can be checked at run-time in a similar fashion. In practice for this particular example I’d use an if-else block to save a couple lines when methods are only available in one state like here.

  • We are now Level Six for checking requirements 1, 2 and 4.
  • However it still does not check that disconnect is ever called on objects that enter state CONNECTED so we’re still level seven on that one.

Level Three

Java provides a construct that can be used to guarantee a piece of code is run before exiting a method: The try-finally block. We can update the client code like this:

Session s = new SessionWrapper();
s.connect();
try {
    // two hundred lines of code using s
} finally {
    s.disconnect();
    s = null;
}
// some more code massaging the result

Let’s review the requirements of the library:

  1. The first method invoked on the object may only be connect. Level Three: someone may still insert an s.ask() or something between lines one and two but it is visually obvious it is incorrect (to someone who knows about library requirements).

  2. The connect method may be used only once on any given instance. Level Three: it is still possible to insert connect invocations in the try-block. No warnings, but obviously wrong.
  3. On an instance where connect() has been invoked successfully, you must eventually invoke disconnect. Level One: guaranteed by the finally block.
  4. No method may be invoked on an instance on which disconnect has been called. Level Two: adding more invocations of s-methods after the finally-block will trigger a compiler warning “s may only be null at this location”.

The last point may be upgraded to Level One with a “try with resources” block given by Java 7. Make SessionWrapper implement AutoCloseable (having close invoke the disconnect method if the object is in state CONNECTED), and make the following change in client code:

try (Session s = new SessionWrapper()) {
    s.connect();
    // two hundred lines of code using s
}
// some more code massaging the result

Now attempting to access s after the try-block makes compilation fail. → Level One (for requirement four).

Level One

The only way to force client code to invoke methods in a certain order is to provide it with an object that, at any given moment, only has permitted methods available.

public interface ConnectedSession {
    Answer ask(Question question);
}

If the state diagram of your object is more complex, create as many interfaces as there are combinations of methods that may be available simultaneously, naming the interface like the corresponding state.

To force invocation of a method without giving access to it, control flow must be reversed with a callback:

public interface Work {
    void runSession(ConnectedSession cs);
}

The wrapper may then be implemented as follows:

public class SessionWrapper {
    public SessionWrapper(Work work) {
        final Session s = new Session();
        s.connect();
        try {
            work.runSession(new ConnectedSession() {
                public Answer ask(Question question) {
                    return s.ask(question);
                }});
        } finally {
            s.disconnect();
        }
    }
}

Finally our client code is changed like this:

new SessionWrapper(new Work() {
    public void runSession(ConnectedSession s) {
        // two hundred lines of code using s
    }
});
// some more code massaging the result

This code has Level One for all requirements: the connect method is called before anything in the client code is executed, and is not accessible anywhere else in client code. Similarly, the disconnect method is called at the end (unless connect fails with an exception), is impossible to forget (the closest equivalent to forgetting it would be omitting the closing curly brace, which would be an immediate compilation failure) and can’t be called from within the runSession method as it is not exposed in the ConnectedSession interface. Finally, no method may be called after the disconnect method is invoked: the SessionWrapper object has no public method (no method at all, really).

At a presentation I once jokingly told my colleagues “when in doubt, add callbacks”. There may be some truth in that after all :)

You may be wondering how the “code massaging the result” may be accessing data constructed in the method. This may be either done with Mutable objects:

Mutable<T> {
    public T val;
}

Before the new SessionWrapper, add:

final Mutable<T> someVariable = new Mutable<>();

In the runSession method you may write it using someVariable.val = something, and read it outside the block with someVariable.val.

Level Seven

You could also alter the design to use abstract methods in SessionWrapper itself, in which case the post-disconnect code may be put in a constructor block of your subclass:

public abstract class SessionWrapper {
    protected SessionWrapper() {
        final Session s = new Session();
        s.connect();
        try {
            runSession(new ConnectedSession() {
                public Answer ask(Question question) {
                    return s.ask(question);
                }});
        } finally {
            s.disconnect();
        }
    }
    protected abstract runSession(ConnectedSession session);
}

used as follows:

new SessionWrapper() {
    // instance variables here. Do not initialise them.
    protected void runSession(ConnectedSession s) {
        // two hundred lines of code using s (and writing to instance variables)
    }
    {
    // some more code massaging the result (and reading from instance variables)
    }
};

This variant lets you share instance variables, but there’s a problem. If you accidentally initialise instance variables as in “int counter=0;”, that initialisation will be performed after the super constructor returns, which is after runSession has terminated, which means an innocuous mistake makes the whole thing fail without warnings or explicit errors. That makes this design collapse down to Level Seven!

The moral of the story is that the more elaborate a Level One design is, the more careful you need to be that there are no hidden assumptions not enforced at compilation which would ruin all your efforts and send you back to Level Seven hell.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" cssfile="">