Programming to an interfaces in Java

The idea behind programming to an interface is to base the code primarily on interfaces and only use concrete classes at the time of instantiation. In this context, good code dealing with e.g. Java collections will look something
like this (not that the method itself is of any use at all, just illustration):

public Set toSet(Collection collection) {
     return Sets.newHashSet(collection);
}

while bad code might look like this:

public HashSet toSet(ArrayList collection) {
   return Sets.newHashSet(collection);
}

Not only the former can be applied to a wider choice of arguments, its results will be more compatible with code provided by other developers that generally adhere to the concept of programming to an interface. However, the most important reasons to use the former are:

  • most of the time the context, in which the result is used, does not and should not need that many details as the concrete implementation provides;
  • adhering to an interface forces cleaner code and less hacks such as yet another public method gets added to a class serving some specific scenario;
  • the code is more testable as interfaces are easily mockable;
  • finally, the concept helps even if only one implementation is expected (at least for testability).

So how can one easily apply the concept of programming to an interface when writing new code having in mind
one particular implementation? One option that we commonly use is a combination of the following patterns:

  • programming to an interface
  • factory
  • builder

The following example based on these principles is a simplified and truncated version of an RPC implementation written for a number of different protocols:

public interface RemoteInvoker {
     CompletableFuture invoke(RQ request, Class responseClass);
}

The above interface is not supposed to be instantiated directly via a factory, instead we derive further more concrete interfaces, one for HTTP invocation and one for AMQP, each then having a factory and a builder to
construct instances, which in turn are also instances of the above interface:

public interface AmqpInvoker extends RemoteInvoker {
    static AmqpInvokerBuilder with(String instanceId,     ConnectionFactory factory) {
         return new AmqpInvokerBuilder(instanceId, factory);
    }
}

Instances of RemoteInvoker for the use with AMQP can now be constructed as easy as (or more involved depending on the builder):

RemoteInvoker invoker = AmqpInvoker.with(instanceId, factory)
.requestRouter(router)
.build();

And an invocation of a request is as easy as:

Response res = invoker.invoke(new Request(data), Response.class).get();

Due to Java 8 permitting placing of static methods directly into interfaces, the intermediate factory has become implicit in the above code replaced with AmqpInvoker.with(). In Java prior to version 8, the same effect can be
achieved with an inner Factory class:

public interface AmqpInvoker extends RemoteInvoker {
    class Factory {
        public static AmqpInvokerBuilder with(String instanceId, ConnectionFactory factory) {
           return new AmqpInvokerBuilder(instanceId, factory);
        }
   }
}

The corresponding instantiation would then turn into:

RemoteInvoker invoker = AmqpInvoker.Factory.with(instanceId, factory).requestRouter(router).build();

The builder used above could look like this (although this is a simplification as the actual one permits defining of up to 15 parameters deviating from defaults). Note that the construct is not public, so it is specifically usable only from the above AmqpInvoker interface:

Related Article: Packages and Inheritance in Java with Examples

public class AmqpInvokerBuilder {
…
     AmqpInvokerBuilder(String instanceId, ConnectionFactory factory) {
         this.instanceId = instanceId;
         this.factory = factory;
     }

     public AmqpInvokerBuilder requestRouter(RequestRouter requestRouter) {
           this.requestRouter = requestRouter;
                 return this;
     }

     public AmqpInvoker build() throws TimeoutException, IOException {
           return new AmqpInvokerImpl(instanceId, factory, requestRouter);
     }
}

Generally, a builder can also be generated using a tool like FreeBuilder.

Finally, the standard (and the only expected) implementation of this interface is defined as a package-local class to enforce the use of the interface, the factory and the builder:

class AmqpInvokerImpl implements AmqpInvoker {
AmqpInvokerImpl(String instanceId, ConnectionFactory factory, RequestRouter requestRouter) {

}
@Override
public CompletableFuture invoke(final RQ request, final Class respClass) {

}
}

Meanwhile, this pattern proved to be very efficient in developing all our new code not matter how simple or complex the functionality is.

Overriding in Inheritance

Overriding in Inheritance is used when you use a already defined method from a super class in a sub class, but in a different way than how the method was originally designed in the super class. Overriding allows the user to reuse code by using existing material and modifying it to suit the user’s needs better.

The following example demonstrates how ClassB overrides the functionality of ClassA by changing what gets sent out through the printing method:

Example:

public static void main(String[] args) {
      ClassA a = new ClassA();
      ClassA b = new ClassB();
      a.printing();
      b.printing();
}

class ClassA {
     public void printing() {
          System.out.println("A");
    }
}

class ClassB extends ClassA {
     public void printing() {
           System.out.println("B");
     }
}

Output:

A
B
Variable shadowing

Variables are SHADOWED and methods are OVERRIDDEN. Which variable will be used depends on the class that the variable is declared of. Which method will be used depends on the actual class of the object that is referenced by the variable.

class Car {
     public int gearRatio = 8;

     public String accelerate() {
          return "Accelerate : Car";
     }
}

class SportsCar extends Car {
     public int gearRatio = 9;

     public String accelerate() {
            return "Accelerate : SportsCar";
     }

     public void test() {

     }

     public static void main(String[] args) {

           Car car = new SportsCar();
           System.out.println(car.gearRatio + " " + car.accelerate());
          // will print out 8 Accelerate : SportsCar
     }
}
Narrowing and Widening of object references

Casting an instance of a base class to a subclass as in : b = (B) a; is called narrowing (as you are trying to narrow the base class object to a more specific class object) and needs an explicit type-cast.

Casting an instance of a subclass to a base class as in: A a = b; is called widening and does not need a type-cast. To illustrate, consider the following class declarations, and test code:

class Vehicle {
}
class Car extends Vehicle {
}
class Truck extends Vehicle {
}
class MotorCycle extends Vehicle {
}
class Test {
     public static void main(String[] args) {

          Vehicle vehicle = new Car();
          Car car = new Car();

          vehicle = car; // is valid, no cast needed

          Car c = vehicle // not valid
          Car c = (Car) vehicle; //valid
    }
}

The statement Vehicle vehicle = new Car(); is a valid Java statement. Every instance of Car is also a Vehicle. Therefore, the assignment is legal without the need for an explicit type-cast.

On the other hand, Car c = vehicle; is not valid. The static type of the vehicle variable is Vehicle which means that it could refer to an instance of Car, Truck,MotorCycle, or any other current or future subclass ofVehicle. (Or indeed, an instance ofVehicleitself, since we did not declare it as anabstractclass.) The assignment cannot be allowed, since that might lead tocarreferring to aTruck` instance.

To prevent this situation, we need to add an explicit type-cast:

Car c = (Car) vehicle;

The type-cast tells the compiler that we expect the value of vehicle to be a Car or a subclass of Car. If necessary, compiler will insert code to perform a run-time type check. If the check fails, then a ClassCastException will be
thrown when the code is executed.

Note that not all type-casts are valid. For example:

String s = (String) vehicle; // not valid

The Java compiler knows that an instance that is type compatible with Vehicle cannot ever be type compatible with String. The type-cast could never succeed, and the JLS mandates that this gives in a compilation error.

Inheritance and Static Methods

In Java, parent and child class both can have static methods with the same name. But in such cases implementation of static method in child is hiding parent class’ implementation, it’s not method overriding. For example:

class StaticMethodTest {

     // static method and inheritance
     public static void main(String[] args) {
        Parent p = new Child();
        p.staticMethod(); // prints Inside Parent
        ((Child) p).staticMethod(); // prints Inside Child
    }

    static class Parent {
        public static void staticMethod() {
             System.out.println("Inside Parent");
        }
    }

    static class Child extends Parent {
        public static void staticMethod() {
              System.out.println("Inside Child");
        }
    }
}

Static methods are bind to a class not to an instance and this method binding happens at compile time. Since in the first call to staticMethod(), parent class reference p was used, Parent’s version of staticMethod() is invoked. In second case, we did cast p into Child class, Child’s staticMethod() executed.

LEAVE A REPLY

Please enter your comment!
Please enter your name here