4

Is there are way we can completely avoid illegal states rather then throwing an IllegalStateException when a method is called when it should not be?

For example:

public interface Process {

    void start();

    void stop();

}

Illegal state would be created if stop() is called before start().

  • 7
    Illegal state would be created if stop() is called before start() -- No, stop() just does nothing, because there is nothing to do. – Robert Harvey May 12 at 17:22
  • How about different instances to represent a state, only providing functions for allowed actions to move to a different state (returning a instance for the new state). This makes it virtually impossible to enter an illegal state. Sadly I can't remember how an architecture like this is commonly named. For machines only having a start/stop state you might be looking for a RAII-like implementation. – Tim Visee May 12 at 17:53
  • Robert Harvey is right, one way to avoid "illegal state" is by simply not considering certain calls as illegal. In your example, one could tolerate a call to stop even if the process does not run, or a call to start even if the process already runs. If that does not help, please edit your question and give a better example. – Doc Brown May 12 at 19:54
  • interface ProcessBuilder {Process start();} interface Process {void stop();} but you can still stop a stopped process. – immibis May 13 at 1:45
  • There is quite some research being done on a concept called "dependent types" which may be used in order to create the kind of design that you wish for without introducing new types and interfaces etc. However so far I think that the only implementations of this is in academic/research languages. But give it a couple more years and way may start seeing this or features inspired by this make it into more mainstream languages. – wasatz May 13 at 13:05
4

Illegal states are that, state. Compilers of most programming languages do not concern themselves with state, only with the types of states.

So, to prevent those errors (or rather, raise them at compile time rather than at run time), you need to encode your state in a way the compiler can understand. For most general purpose programming languages this is going to be a real chore to do and a major pain to use, if not even impossible.
There are, however, languages and compilers that model the possible values of a variable/object in the type system and can provide a lot of compile time safety in that regard (official term: dependent types). The Wikipedia Page lists a few example languages.

  • dependent types are necessary in general to solve these class of problems but this specific one can be solved without dependent types – jk. May 14 at 14:59
  • 1
    @jk the specific case in the OP was an example – marstato May 14 at 16:03
  • This is just an example. I wanted to get a generic solution. – amanmehara May 14 at 16:12
10

You can avoid illegal states by starting in a legal state and disallowing transitions into illegal states.

The problem is enforcing that in a system complex enough to get useful work done.

The code you posted is trivial enough that it can be fixed simply by adding another type to represent your missing state.

enter image description here

You seem to be using some Java like language so here's one way to fix your code:

public class Stopped {
    Started start() { ... }
}

public class Started {
     Stopped stop() { ... }
}

Here the compiler is enforcing that these methods be called in order. I don't even have to look at the using code.

But add a simple feature and watch it all fall apart. Lets say we're simulating an internal combustion engine. The kind in your car. That's seems like it should be fine. Those start and stop all the time.

Lets add a throttle. You know, a gas peddle. When started it makes the engine rev. When stopped it floods the carburetor.

That actually works fine. Add a little polymorphism. Started and stopped now both implement a throttle interface. Spiffy.

Then someone up and decides that throttles are not binary. They go from 0% to 100% open. So they decide to pass numbers from 0 to 100 to the throttle method. Sounds fine. But they used an int.

Can you see the problem yet? Sure 0 works. 100 works. Anything in between works. So we're fine right?

Well no we're not. Before, we didn't have to look at the complex using code. We could prove our code stays in valid states just by reviewing two classes. Now we have to review everything that touches our code.

The problem, of course, is that an int allows more than 0-100. We can pass negative numbers, maybe hoping to go in reverse, and we can ask the engine to give 110%. While that looks great on motivational posters it can easily put us into an invalid, undefined, state.

Now you can, and should, try to fix this with validation but this is what drives us to give up on proving that code works and instead lean on testing. Our code is going out into a wild and wooly world full of code that we may never see. When that code touches our code it could pass whatever.

So what you do, to prevent invalid states, is control transitions, validate input, and test the hell out of it.

A single 4 byte int has billions of states. You going to test all that? Of course not. What you do is something called state space partitioning. Which is a fancy way to say test the limits of your boundaries. In this case you'll want throttle tests for -1, 0, 100, and 101. That tests each side of your upper and lower boundaries.

That should give you something called code coverage. Each branch of code gets exercised by tests. Even the ones that are supposed to throw an error in your face.

This still won't that prove that you can never enter an invalid state. But it's the closest we can come in a language like this.

If you need something more formal and provable, look into Scala.

  • 2
    And then someone calls stop twice on the same object. Boom! – Deduplicator May 12 at 22:40
  • 2
    But you retain the object you called the transition on. And thus you can call it again. – Deduplicator May 13 at 0:00
  • 2
    @Deduplicator yes and by doing so you haven't changed state. You can call it all day long and never change state if you don't take what it gives you. You're confusing state with identity. – candied_orange May 13 at 0:08
  • 3
    @Deduplicator In Rust you can prevent them being called twice by making the functions consume self when they are called. Any attempt to call one of them twice will be prevented at compile-time. I'm not sure if any other languages support this sort of functionality. – Sean Burton May 13 at 9:40
  • 5
    @Bwmat Trusting programmers to follow documentation is tantamount to trusting foxes to maintain the chicken coup. Please use compiler errors to help me fall into a pit of success when you can. – candied_orange May 13 at 19:27
3

Stopping in a stopped state would just be a nop operations, so do nothing. No IllegalStateException need be thrown, just return immediately from the method if the system is in a state where the stop operation would be meaningless.

Similar with the start operation. If the system is already started, it would be a nop operation.

Of course for a more complex system, with more states and operations, things can get tricky. What to do when you for example restart is called, and the system isn't started? Do you then relegate to start instead, of throw an IllegalStateException? That's a design decision, and would depend at least in part on the complexity of the states involved. If restarting is more than just stopping and starting, you can't just do a start if the system isn't stopped for example.

  • 1
    This is the right answer in this case. What's the harm of starting something that was already started? The client code shouldn't care about the messy details - that's what the interface is for. Just start the "process" and if the process is already started, then, well, I guess that's an easy method call. – Greg Burghardt May 13 at 16:17
  • 2
    @GregBurghardt Have you ever tried starting a car that was running? I'm not sure how bad it is but it makes a really unpleasant sound. – JimmyJames May 13 at 18:10
  • 1
    @JimmyJames: It all depends on the use case scenario. Frankly I think with all the high tech gizmos and whatcha-ma-doodles that today's cars have, someone should program in a guard to prevent that awful sound from happening. Maybe someone from Ford or GM should have asked this exact question 20 years ago. Too bad StackExchange wasn't around back then... – Greg Burghardt May 13 at 19:41
  • 1
    @GregBurghardt Total agreement on 'it depends'. If I try to start something that is already started, maybe it's OK because all I care about is ensuring it has been started. In another case, the fact that it's still running is a sign that there is a major issue. I think my team ran into this actually: on an upgrade, the process never stopped so even though the binaries were replaced, it was still running an old version. A failure on starting something that was running wasn't a terrible thing in that case. – JimmyJames May 13 at 21:00
  • @JimmyJames if I turn the ignition key in my car to start when it's already running, nothing happens, it just goes on and keeps running normally. – jwenting May 14 at 4:06
1

The right answer is complicated, but it seems to be no, depending on how you look at things. You may be talking about development time or execution time. I will talk only about execution time.

You are implementing a state machine. The state machine, at any point of execution, has to have a current state.

Transitions are triggered by the occurrence of events originating from outside your state machine. A state of your state machine will be "illegal" if the outside of your state machine does not make events occur in the order the specifications say. It may happen, in any case, be it because you program launches while the outside world is not in its initial state, because the ouside world is buggy, or because events are maliciously unordered.

The only choice for you have, for your system to be robust, is to map every state/event pair to a pair consisting of a triggered behaviour and a new state.

The triggered behaviour may be an exception thrown.

1

[*caveat: I assume process -> concurrency]

To me, there's no generic way to avoid illegal state because there is no general definition of 'illegal state.' Illegal states don't figure in Turing machines or the lambda calculus. If an illegal state exists, it exists via design.

Illegal states are the result of a particular implementation of a particular business logic or application domain. For example, the business logic of Linux processes doesn't worry whether a process is started or exists when kill sends a signal to a process. reference:

$> kill -0 2733
bash: kill: (2733) - No such process
$> kill 2733
bash: kill: (2733) - No such process

To me, signals look quite a bit like message passing if I squint hard enough. The behavior of Linux/Unix signals may or may not be what you want. But it is worth considering that the way in which Linux/Unix handle stopping processes reflects the design thinking of many very competent engineers over several decades.

Looking at the design of Linux/Unix processes the interface seems to leave some design questions.

  • What is a process doing when it's started?
  • How does start() relate to process creation?
  • What is a process doing when it's stopped?
  • How does stop() relate to process destruction?

The answers can form the basis for determining what is and isn't an illegal state.

protected by gnat May 14 at 12:34

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.