Code to Interfaces, Not to Implementations

A common mantra of object-oriented development is that one should code to the interface, not the implementation.  But what does it mean?  What does it look like in Java?  And why does it matter?  Let’s explore this mantra in detail and see why it is considered a best practice.

What does it mean?

Quite simply, it means that your code should be “class inclusive” rather than “class exclusive”.  In other words, don’t be a class snob.  When specifying a method parameter or return value — even if you think you know what class you will eventually instantiate or return during your implementation, or what class others are likely to pass in to your method — if that class is one of many implementing a common interface (or extending an abstract class), then it is better to declare the field or variable as an instance of the lowest level interface or abstract class upon whose behaviors your code depends, rather than as an instance of the implementation type.  Think of it like finding the least common denominator when simplifying fractions in math class, or finding the simplest tool needed to perform a certain task.

Here’s a real-world analogy: suppose you’re on vacation, and when you arrive at the hotel, you realize that you’ve forgotten to pack your turbo-charged, battery-operated electric toothbrush with spinning heads, and that you will need to brush your teeth at some point before you return home.  Do you really need to go out and buy another turbo-charged, battery-operated electric toothbrush with spinning heads?  Of course not.  You really just need a toothbrush.  It’s the minimum tool that you need in order to perform the task of brushing your teeth.

What Does it Look Like in Java?

Let’s first take a look at an example of what not to do.  Have you ever had to maintain or review someone else’s code and they did something like this?

public void printWords(ArrayList<String> words) {
  for(String word : words) {
     System.out.println(word);
  }
}

public void printKeys(HashMap<Integer, String> pairs) {
  for(Integer key : pairs.getKeys()) {
    System.out.println(key);
  }
}

Did you immediately get the feeling that the code just didn’t look right?  Did you cringe as soon as you saw it?  Could you smell it?  If you answered “yes”, then you are already well on your way to understanding this basic principle.  If you answered “no”, read on.

Suppose you want to print a list of words that is stored in a LinkedList?  Or print all the keys in a TreeMap?  You can’t use the above methods as written, because a LinkedList is not a descendant of ArrayList, and TreeMap is not a descendant of HashMap.

Now look at the implementations above.  Does the printWords method perform any operations on its parameter that are exclusive to an ArrayList?  Or does printKeys perform any operations on its parameter that are exclusive to a HashMap?  The answer is clearly no in both cases.  Here is a better way to write these methods that is inclusive of more common types:

public void printWords(List<String> words) {
  for(String word : words) {
    System.out.println(word);
  }
}

public void printKeys(Map<Integer, String> pairs) {
  for(Integer key : pairs.getKeys()) {
    System.out.println(key);
  }
}

Toothbrushes Revisited…

To further illustrate this principle, let’s revisit the toothbrush analogy.  The following is a sample class interface and class hierarchy fitting the scenario described above:

public interface Toothbrush {
  void brushTeeth();
}

public interface ElectricToothbrush extends Toothbrush {
  void charge();
}

public interface BatteryPoweredToothbrush 
            extends ElectricToothbrush {
  void replaceBattery();
}

public class BasicToothbrush implements Toothbrush {
  public void brushTeeth() {
    . . .
  }
}

public class TurboChargedBatteryPoweredSpinningToothbrush
            implements ElectricToothbrush {
  public void brushTeeth() {
      . . .
  }
  public void charge() {
      . . .
  }
  public void replaceBattery() {
      . . .
  }
}

And here is a simplistic example showing how one might use the Toothbrush interface and class hierarchy in everyday life and while on vacation.

A Day in the Life…

public class DayInTheLife() {
  public void getUp() {
      . . .
  }
  public void brushYourTeeth(Toothbrush brush) {
      brush.brushTeeth();
  }
  public void takeShower() {
      . . .
  }
  public void writeAwesomeJavaCode(int hours) {
      . . .
  }
  public void eat(Meal meal) {
      . . .
  }
  public void chill(int hours) {
      . . .
  }
  public void sleep(int hours) {
      . . .
  }
  public void doFunStuff(int hours) {
      . . .
  }
  public void buyStuff(Object... obj) {
      . . .
  }
}

Today, a Regular Day…

public class RegularDayAtHome() {
  private Meal breakfast;
  private Meal secondBreakfast;
  private Meal lunch;
  private Meal dinner;
  public RegularDayAtHome(Meal[] meals) {
      //instantiate Meal fields
  }
  public void main() {
      DayInTheLife today = new DayInThLife();
      today.getUp();
      today.takeShower();
      today.eat(breakfast);
      Toothbrush tb = 
          new TurboChargedBatteryPoweredSpinningToothbrush();
      today.brushYourTeeth(tb);
      today.writeAwesomeJavaCode(2);
      today.eat(secondBreakfast);  //hobbits
      today.sleep(1);  //power nap
      today.writeAwesomeJavaCode(3);
      today.buyStuff(lunch);
      today.eat(lunch);
      today.writeAwesomeJavaCode(3);
      today.eat(dinner);
      today.writeAwesomeJavaCode(1);
      today.chill(4);
      today.sleep(8);  //good night
  }
}

Vacation Day!

public class VacationDay() {
  private Meal brunch;
  private Meal dinner;
  public VacationDay(Meal[] meals) {
      //instantiate Meal fields
  }
  public void main() {
      DayInTheLife someday = new DayInThLife();
      someday.getUp();
      someday.takeShower();
      someday.eat(brunch);
      //oops, I forgot my toothbrush
      Toothbrush tb = new BasicToothbrush();
      someday.buyStuff(tb);  //buy a toothbrush
      someday.brushYourTeeth(tb);
      someday.doFunStuff(5);
      someday.buyStuff(souvenirs);
      someday.sleep(3);  //I need a nap!
      someday.doFunStuff(2);
      someday.eat(dinner);
      someday.chill(2);
      someday.sleep(10);  //good night
  }
}

Why does it matter?

Coding to interfaces increases the reusability of your code.  When it comes down to specifying a method parameter, you are primarily interested in its behaviors and its potential use as a parameter to be passed to another method.  Don’t paint yourself into a corner by unnecessarily restricting the classes your methods will accept.

Recap

Learning to consistently code to interfaces is an important part of a developer’s arsenal and is one of many techniques that separate senior developers from junior or entry-level developers. I could write an entire book chapter on this subject, with lots of examples, but I think I’ve hit the main points.  So when designing and writing a piece of code, remember the following:

  • Be inclusive rather than exclusive!
  • Don’t be a class snob!
  • Reusability is king!
Advertisements