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
).