About two weeks ago, I spoke about composition as a software engineering concept. Today’s topic is about a tool for bringing two components together: the interface.

In general, an interface is how two components communicate with each other. In a peer-based system, this means that a client and a server don’t talk to each other directly. Rather, the client goes through some well-defined entry point. With REST, for instance, clients can always assume that POST and PATCH mean very specific things, without having to know anything about the server. This way, the server can freely change anything about its own implementation without affecting any would-be client, so long as it continues to follow the REST protocol.

Most object-oriented programming languages have an operator named interface. This operator allows you to define some sort of contract about what methods will be available. Basically, any class that implements an interface is obligated to implement all the methods defined in it, and anyone who uses that implementing class has the guarantee that it will always have those methods. Think of it as a specification: a piece of paper stating that so-and-so object will ALWAYS have the listed methods available for use.

It’s very useful for keeping your objects decoupled. And if you recall your SOLID principles, that is a very good thing.

So what does this look like in practice? Say, for instance, you have a class that looks like this:

import org.foo.bar.UserDao

class UserRegistrationService {
  private UserDao userDao

  UserRegistrationService(UserDao userDao) {
    this.userDao = userDao;
  }

  void registerUser(String id) {
    if (validate(id)) {
      userDao.createUser(id);
    }
  }

}

And you have another class that looks like this:

package org.foo.bar

class UserDao {
  
  void createUser(String id) {
    // do something to create a user in the database.
  }

}

UserRegistrationService is tied to that specific UserDao class. Whoever wrote UserDao is forever bound to maintain it, since UserRegistrationService is calling the method of that specific UserDao class in that specific org.foo.bar package. It’s difficult to make any significant without affecting the code in UserRegistrationService. Moreover, you can’t switch UserDao for a similar class in a different package since UserRegistrationService expects the one from org.foo.bar.

We can decouple these two components by introducing an interface.

package org.foo.bar

interface UserDao {
  
  createUser(String id)

}

UserRegistrationService will import this interface and act upon it.

import org.foo.bar.UserDao

class UserRegistrationService {
  private UserDao userDao

  UserRegistrationService(UserDao userDao) {
    this.userDao = userDao;
  }

  void registerUser(String id) {
    if (validate(id)) {
      userDao.createUser(id);
    }
  }
  
}

The old UserDao class gets renamed to UserDaoImpl, and implements the UserDao interface.

import org.foo.bar.UserDao

class UserDaoImpl implements UserDao {

  // UserDaoImpl must implement this method to avoid a compile-time error
  void createUser(String id) {
    // do something to create a user in the database.
  }

}

UserRegistrationService no longer cares about the UserDao class (now renamed to UserDaoImpl). It doesn’t care what actually happens internally, when it calls the createUser() method. It doesn’t even have to be in the same package. All it knows is that it’s expecting some object that matches the specification in the UserDao interface. The developer could inject any implementation as a dependency, and could switch this implementation as their needs require.