Using the Object type for a method parameter or instance variable in Java application code is a choice that will later haunt you. The temptation to use Object strikes when common functionality needs to be extracted from otherwise unrelated classes and it is impossible for these classes to share a common interface. Seeing this problem can indicate that a larger refactoring is in order, however for now we’ll focus on this specific issue.
In this example we have mostly unrelated classes and we want to extract a String representing a sound from each, for example a Dog would get us “woof”. Here’s a sample method that uses instanceof and calls the appropriate method.
public String getInfo(Object input) { return "Sound = " + getSound(input) + ", id = " + getId(input); } // Counterexample: don't do this! private String getSound(Object input) { if (input instanceof Animal) { return ((Animal) input).getSound(); } else if (input instanceof Vehicle) { return ((Vehicle) input).getNoise(); } else if (input instanceof Mime) { return ""; } else { throw new IllegalArgumentException("Can't get sound"); } } ...
There are a number of issues with this method:
- Loss of compile time type checking: An otherwise innocent change of an instance of class Animal to Optional<Animal> would cause this code to explode, possibly weeks after the change was introduced. It is always preferable to find errors at compile time instead of at runtime.
- Fragile to maintain: If later you need to add a Bird type whose sound is accessed via a getSong() method you may forget to add an if/else block for Birds. Again you will not find out about your error until runtime.
- Readability suffers: The method signature, getInfo(Object), does not make it clear what types are valid input.
Here’s a suggestion for how to refactor this code, assuming that we don’t know the compile time type of the object whose sound we need and thus can’t make a new method for each type (i.e. getSound(Animal), getSound(Vehicle), …), and that we can’t alter the object’s interface. Instead of using Object we define a new Soundable class that provides an interface to objects that we need the sound of. I think of this as a “hidden adapter” pattern since Soundable is an adapter whose concrete implementations are hidden in private classes.
public abstract class Soundable { // The goal is to keep refactoring until this method can be deleted! public abstract Object getDelegate(); public abstract String getSound(); public static Soundable from(Animal animal) { return new AnimalSoundable(animal); } public static Soundable from(Vehicle vehicle) { return new VehicleSoundable(vehicle); } public static Soundable from(Mime mime) { return new MimeSoundable(mime); } // You could also call this class "AnimalToSoundableAdapter" private static class AnimalSoundable extends Soundable { private final Animal delegate; private AnimalSoundable(Animal animal) { this.delegate = animal; } @Override public String getSound() { return delegate.getSound(); } @Override public Object getDelegate() { return delegate; } } private static class VehicleSoundable extends Soundable { private final Vehicle delegate; private VehicleSoundable(Vehicle vehicle) { this.delegate = vehicle; } @Override public String getSound() { return vehicle.getNoise(); } @Override public Object getDelegate() { return delegate; } } // ... Another class for Mime. }
Here’s our new getInfo() method. A good next step would be to refactor getId(Object) just as with getSound(Object).
public String getInfo(Soundable soundable) { return "Sound = " + soundable.getSound() + ", id = " + getId(soundable.getDelegate()); }
The result is still not the purest code but it is much improved. The techniques can be applied more generally to avoid instanceof (always a good idea) and replace if/else with polymorphism. We also created a better separation of class responsibilities by moving complexity out of the class containing the getInfo() method.
Here’s a rundown of how the major problems with the original method are solved:
Loss of compile time type checking: We can now only create instances of Soundable from well defined types and IllegalArgumentException is no longer lurking.Fragile to maintain: If we add a Bird type we will get a compiler error if we forget to make a corresponding modification to Soundable.Readability suffers: The new getSound(Soundable) method signature makes it clear what data the method is meant to act on.
The goal of this example is to show ideas for how to improve code in small steps and think about good design.