What is the Java type system for? – Case coverage

So what might a programming language with strong static named types be useful for? I recently asked an interview question about how the observer pattern could be implemented in a situation involving the occurrence of a number of different types of events. This answer was suggested:

interface EventListener {
    void eventOccurred(Event event);
}
 
class MyEventHandler implements EventListener {
    void eventOccurred(Event event) {
         if(event.getType() == EventType.subtypeOne) {
            doTypeOneThing((EventSubtypeOne) event);
         } else if(event.getType() == EventType.subtypeTwo){
            doTypeTwoThing((EventSubtypeTwo) event);
         } else {
            throw new RuntimeException();
         }
    }
}

There are a number of issues with this design, however the main issues that occurs in the kind of large programs I often work on is: when a new type is added how do you make sure that all of the types of events are handled in every implementation of EventListener?

The suggested solutions are often along the lines of writing test cases for each of your EventListener implementations and taking the absence of an exception as success. But then how do you make sure you have updated all of your tests correctly?

A simple, easy to understand, and Object Orientated way to resolve this problem is:

interface EventListener {
    void eventOneOccurred(EventOne event);
    void eventTwoOccurred(EventTwo event)
}
 
class MyEventHandler implements EventListener {
    void eventOneOccurred(EventOne event) {
            doTypeOneThing(event);
    }
 
    void eventTwoOccurred(EventTwo event) {
            doTypeTwoThing(event);
    }
}

Important points to notice about this solution:

  1. No casting. Since we never represent the event as a generalised abstraction, we never need to downcast the event. We don’t even need a base type for event unless we have some use for that abstraction in our system.
  2. No runtime error. As long as all the code is compiled together (or at least has binary compatibility) then no runtime error will occur because our type system proves that all the types of events are handled.
  3. Updates are easy. When we add a new event type, we add the new method to the interface and the compiler will tell us where all the implementations are! This is very fast if you have an IDE with incremental compilation and all the code. But it will work safely even in situations where the people implementing your interfaces are distributed and you have no possibility of updating their code. When they try to compile against the new version of your library they will immediately find out about the new feature (if they like it or not)!

Of course, you don’t always want these properties in every design. But when you do want your design to have these properties, this is how you can do it.

100% branch and statement coverage does not mean the code is fully covered

Standard Disclaimer: This discussion is about high reliability software for situations where software failures can have significant impacts. The ideas discussed are not necessarily appropriate or cost effective for most software products.

When refactoring high reliability software it is often important not to introduce any observable behavioural changes – no bugs. But how do you go about it?

One answer is to have very high test coverage. Ensuring a high line and branch coverage is not enough to ensure that your refactoring is safe. The reason for this is that even 100% branch coverage might only test a very small subset of the possible executions of a program. A simple example:

int myFunction(int x)
{
   int result = 0;
   if(x % 2 == 0)
   {
       // A (even)
       result++;
   }
   else
   {
       // B (odd)
       result--;
   }
 
   if(x % 3 == 0)
   {
       // C (divisible by 3)
       result++;
   }
   else
   {
       // D (indivisible by 3)
       result--;
   }
 
   return result;
}

So, we can test all the branches with the tests values 2,3,7.

  • 2 – branches A,D
  • 3 – branches B,C
  • 7 – branches B,D

So, even with this simple code where there is 100% branch and statement coverage, it does not cover the path “A,C”. An additional test case is required. For example the value (x == 6) would cover “A,C”.

In practice, there can be a very large number of paths through a program, so exhaustively testing them can be either very expensive or completely impractical.

Fail Fast – Fail During Compile

One of the fastest ways for faulty software to fail is at compile time.

Here is an example of slow failing code, it will fail at runtime if it comes across an implementation of A that it did not expect. This can happen if a developer adds a new implementation of A but forgets to update this code (or is not aware of it, or it is located in some other dependent library).

interface A { }
class B implements A { }
class C implements A { }
 
void doSomething(A a) {
   // not recommend
   if(a instanceof B) {
      doMyBThings((B) a);
   } else if(a instanceof C) {
      doMyCThings((C) a);
   } else {
      throw IllegalArgumentException("unexpected kind of A");
   }
}

One way to move these failures to compile time would be to move the responsibility for doing my B things and doing my C things on to the classes B and C respectively.

interface A { void doMyThings(); }
class B implements A { void doMyThings(){ /* do my B things */ } }
class C implements A { void doMyThings(){ /* do my C things */ } }
 
void doSomething(A a) {
   a.doMyThings();
}

If I add a new type D the compiler forces us to implement doMyThings()

class D implements A { void doMyThings(){ /* do my D things */ } }

Sometimes it is not appropriate to put the responsibility to doMyThings() onto A. It might create undesirable coupling (a dependency) or have some other undesirable property. I can maintain the fail fast property in in other ways.

interface A { void doThings(Things things); }
class B implements A { void doThings(Things things){ things.doBThings(this); } }
class C implements A { void doThings(Things things){ things.doCThings(this); } }
 
interface Things {
      void doBThings(B b);
      void doCThings(C c);
}
 
void doSomething(A a) {
   a.doThings(new Things(){
      void doBThings(B b){ /* do my B things */}
      void doCThings(C c){ /* do my C things */}
   });
}

Adding a new type D, the compiler forces me to implement doThings on D, which leads me to add a new method onto the Things interface, which forces any implementers of Things to handle D types.

class D implements A { void doThings(Things things){ things.doDThings(this); } }
 
interface Things {
      void doBThings(B b);
      void doCThings(C c);
      void doDThings(D d);
}
 
void doSomething(A a) {
   a.doThings(new Things(){
      void doBThings(B b){ /* do my B things */}
      void doCThings(C c){ /* do my C things */}
      void doDThings(D d){ /* do my D things */}
   });
}

However, a user might not want to handle every new implementation of A, I can provide default implementations of the methods:

class BaseThings implements Things {
      void doBThings(B b) { };
      void doCThings(C c) { };
}
 
void doSomething(A a) {
   a.doThings(new BaseThings(){
      void doBThings(B b){ /* do my B things */}
      void doCThings(C c){ /* do my C things */}
   });
}

When I add D I also add a default implementation, so I do not have to add any new handling code:

class BaseThings implements Things {
      void doBThings(B b) { };
      void doCThings(C c) { };
      void doDThings(D d) { };
}
 
void doSomething(A a) {
   a.doThings(new BaseThings(){
      void doBThings(B b){ /* do my B things */}
      void doCThings(C c){ /* do my C things */}
   });
}

I can choose to either have my code break when new implementations of A are added (by extending Things) or not to break (by extending BaseThings).