Java Design Patterns – Forcing ‘Program to an Interface’ Without Using Interfaces

designdesign-patternsinterfacesjavaobject-oriented

In java 1.8 they have wonderful new "default interface methods". In 1.6 how close can we come? The goal: use code to keep clients from being able to tell that a class is not a java interface. If we preserve the right to refactor the class name into a Java Interface we don't need to create a Java interface now. We just provide the class. Later, refactoring so the class name becomes an interface wont even touch client code.

From Telastyn's answer in Understanding "programming to an interface":

  • The "Dependency Inversion Principle" says that an object shouldn't control the creation of its dependencies, it should just advertise what dependency it needs and let the caller provide it. But it doesn't specify whether the dependency should be a concrete type or an interface.

I want to take advantage of this lack of distinction. I also want to interpret the principle "program to an interface" to mean, "I shouldn't be able to tell if any client code is working with a java Interface or a java Class". So much so, that later someone could swap out the class for a java interface with the same name and no client code would need to be touched. That may seem overly strict and not exactly what was meant by these design principles (which try to be ignorant of java's particular quirks) but I'd love to pull it off.

Here's why: Our development environment doesn't use continuous integration. Releases that force everyone to integrate their code happen once every… well it hasn't happened in all the years I've been working there. Worse than that, testing is hugely manual for reasons I wont even attempt to justify. This means: writing a class that will get used by other people is essentially published because changing it will break someones stream or tested code that is closed and closed hard. This means: no refactoring the signature of anything public on a shared class. It feels the same as I expect writing an API for a library must feel.

I believe in YAGNI. I don't want to do a BDUF. But this situation tempts me to do silly things like put a Java Interface (big I) on every shared class even though, for now, they only have one implementation. This is against YAGNI, it annoys developers who consider an unneeded interface to be clutter.

What I really want is the freedom to add that Interface, and maybe alternate implementations, later if needed. I want to be 100% sure I can do this even when I can't even see all the code that might be written against my widely shared class. I can only be that sure if I use the compiler to FORCE users to program to my interface (little i).

The question: Can you break this protection w/o using reflection from outside the trusted package? Is the following the right way to achieve this? In this environment you only get one shot at getting this right. I want to know if there is anyway to lock me down to the old implementation from outside the trusted package. All the commented code is "future" code. Except for the errors in main, which should always be errors.

Here we see someone trying their best to lock us down to an implementation:

package untrusted;

import trusted.AmIAnInterfaceWithDefaultImplementation;
import trusted.WhoKnowsWhatImplementation;

public class Activate {

    public static void main(String[] args) {
        System.out.println( WhoKnowsWhatImplementation.thisProvides().letsFindOut() );

        AmIAnInterfaceWithDefaultImplementation i =                             
            WhoKnowsWhatImplementation.thisProvides();

        System.out.println( i.letsFindOut() );

        //Error: AmIAnInterfaceWithDefaultImplementation i = new AmIAnInterfaceWithDefaultImplementation();
        //Error: The constructor AmIAnInterfaceWithDefaultImplementation() is not visible
    }
    //Error: public class ImGonnaForceYouToBeAClass extends AmIAnInterfaceWithDefaultImplementation {}
    //Error: The type ImGonnaForceYouToBeAClass cannot subclass the final class AmIAnInterfaceWithDefaultImplementation
}

Prints: Default implementation twice.

Here we choose the implementation to provide in a static factory method. It's only because we're in the same package that the constructor works.

package trusted;

public class WhoKnowsWhatImplementation {
     public static AmIAnInterfaceWithDefaultImplementation thisProvides() {
        return new AmIAnInterfaceWithDefaultImplementation();
        //return new IAintNoInterfaceNoMore();
    }
}

The class below provides both the interface (small i) and the implementation but because the only constructor is protected and the class is final nothing outside the trusted package can force the interface and implementation to stay together.

package trusted;

/** 
 *  Please don't instantiate directly as this may become an Interface.
 *  Use WhoKnowsWhatImplementation 
 */ 
public final class AmIAnInterfaceWithDefaultImplementation {

    protected AmIAnInterfaceWithDefaultImplementation(){}

    public String letsFindOut() {
        return "Default implementation";
    }
}

//public interface AmIAnInterfaceWithDefaultImplementation{
//  public String letsFindOut();
//}

We're free to switch to being an interface and push the implementation out to a new class

package trusted;

//public class IAintNoInterfaceNoMore implements AmIAnInterfaceWithDefaultImplementation {
//
//  @Override
//  public String letsFindOut() {
//      return "Shiny new implementation";
//  }
//}

The need for the 'trusted' package comes from a desire not to have the static factory method live on the same class it's instantiating. It would have to move when we switched to a Java Interface which would impact client code. That is why the constructor had to be package accessible.

Can you lock this down as a class from outside the trusted package without using reflection? Is your way of locking it down something that is reasonable to protect against? What else would you do to protect against getting locked down? Is there a better approach in java 1.6?

I'd also be interested to know if there is anyway to protect against reflection beyond the java doc begging you not to instantiate it directly but I suspect there isn't.

Many designs fail to adhere to all the design principles. This one violates the open closed principle: classes should be open to extension but closed to modification. However, this is an attempt to ensure that the classes clients can stay closed even if it changes into a Java Interface.

The example refactoring shown here switches to an interface and implementation class. It's worth noting that instead it might be decided that the class should actually be abstract. When we switch to java 1.8, it might even end up as an interface with default methods on it. The whole point is to preserve the right to make this decision later so we don't have to make it now when we don't know which to do and don't really care.

EDIT: I appreciate all these answers. Before I accept one, what I'm waiting for is analysis of how vulnerable this approach is to getting locked down to being a class. I'm willing to accept one that shoots down this whole idea by showing me code that dooms it to forever be a class. Accepting one that claims it's a sound idea is a nice ego boost but I'd really rather fail here than in the code base. So convincing me that it's a sound idea is the harder job.

What I've learned from these responses:

  • Converting from the class to the java interface requires that clients recompile
  • The class must not expose any more than the java interface could (e.g., no static methods)
  • Some will insist classes implement Java Interfaces upfront regardless of when they are needed, sigh

The need to recompile the client code strikes me as the most significant issue to be aware of so I'm accepting Maarten Winkels answer. Thank you all and happy coding.

Best Answer

I don't see any way to "lock down the interface to a class" in your code, but I do think you need to consider two things:

  1. Your clients would still need to re-compile their code, even though the client source code doesn't have to change: invoking a method on an interface uses a different bytecode operation then invoking a method on an instance.
  2. What would prevent your coworkers from creating a class in the trusted package and invoking the constructor there?
Related Topic