Your interface is not an abstraction

Your interface is not an abstraction

Interfaces are great. You can write a library, bind your code internally to interfaces and handout the library to consumers which in turn just implement the interfaces. You, as a library implementer, are none the wiser about any implementers. All you know is your code and your interface. Neat!

Or how about having a caching layer in production which becomes a pass-through layer in local dev environment? Just write an interface for the caching and switch out the concrete implementation based on profiles. All your code knows is the interface and the implementation gets swapped dynamically. Nice!

And then there are these use-cases with an interface and exactly one implementation:

package com.test.unicorn.service;

public interface IUserService {
  User findUserByName(String username);

  void calculateUserBusinessLogic(User user);

  void deleteUser(User user);
}
package com.test.unicorn.service.impl;

import com.test.unicorn.domain.User;
import com.test.unicorn.service.IUserService;

public class UserServiceImpl implements IUserService {
  @Override
  public User findUserByName(String username) {
    // Logic
  }

  @Override
  public void calculateUserBusinessLogic(User user) {
    // Logic
  }

  @Override
  public void deleteUser(User user) {
    // Logic
  }
}

Mentally duplicate this code for all existing “-Service” and “-Repository” classes and your code-base (at least in terms of classes) effectively doubled. On top of that, if you were to remove such interface (except a little refactoring of class names), your program will still run perfectly fine. This makes such interfaces effectively dead code.

Concise code

The interface above is also a shallow interface (Ousterhout, pp.39-40), providing no abstraction. Further, having only one implementation, it also does not provide any benefits in swapping out concrete implementations. It is also not better at hiding any details compared to just a class with private methods.

A common argument for such interfaces is to make classes testable. Introducing code to your application solely for the purpose of making your code testable should be the last chosen option. Doing that will entangle testing and business code and might even lead to people wondering why certain code or certain classes are there in the first place, thinking they are required by your domain. Which in turn moves your code base further from your domain and makes it harder to mentally map to your business. For testing the concrete class above, we can just easily mock it without introducing any new code into our business logic.

Where to go from here?

Before creating an interface next time, think if you really need it. Just as you would think before using inheritance or whether some logic should be in this or that class. The smaller and more concise your code base is, the easier it is to navigate and understand.

So you can go from this

Complicated unnecessary interface structures with impl to this Tidy class structure without interfaces

Much neater!

In case you really need to extract a class to an interface if you do require two implementations suddenly, Intellij IDEA gives you an automated way to do so (Extract Interface with IDEA) and the code changes will be minimal. Blindly creating an interface for every class “just in case” will just bloat your code base unnecessarily.

References

  1. Ousterhout, John. A Philosophy of Software Design. Yaknyam Press, 2018.
  2. Extract Interface with IDEA. https://www.jetbrains.com/help/idea/extract-interface.html. Accessed 25.10.2020.