DEV Community

loading...

Say no to Interface pollution - Interface Segregation Principle

Abhinav Pandey
All things Java!!
・3 min read

The Interface Segregation Principle (ISP) states that:

Clients should not be forced to depend on interfaces that they don't use

In a nutshell, this means that clients should not be using interfaces whose methods(most of them) they do not use.

Interface Pollution

Adding methods to an interface even if they are not required by the clients using that interface and are not implemented by many of the implementations of the interface.

Some common signs are:

  1. Empty method declarations in implementations
  2. Methods throwing UnsupportedOperationException

ISP suggests that we should write highly cohesive interfaces so that when they are implemented, a class will not be forced to implement methods it does not need.

Whenever a situation arises where a class needs only a subset of interface methods, it could be better to either break the interface into smaller interfaces OR create a different interface which caters to the specific needs of the implementing class.

In practice, you will find that Interface pollution is often accompanied by a violation of either SRP or Liskov's substitution principle.

A familiar looking interface

Let's say we are building an ecommerce application. One of the first things to do is to have a product database where you can store and find products. And because we think we would be dealing
with a database, why not create an interface for all database transactions. So here it goes!

public interface PersistenceService {
    /*
     * Saves an entity to a Database
     */
    void save(Entity entity);

    /**
     * Finds an entity from database by id
     * @param entity id
     * @return entity
     */
    Entity findById(String id);

    /**
     * Finds an entity from database by id
     * @param entity name
     * @return entity
     */
    Entity findByName(String name);
}
Enter fullscreen mode Exit fullscreen mode

Pretty self explanatory methods. One method saves the entity to the database, one finds them by id and another one finds them by name.

A good implementation

And not since we have the interface ready, we can implement it for our products table.

public class ProductPersistenceServiceImpl implements PersistenceService {

    @Override
    public void save(Entity entity) {
        // TODO Auto-generated method stub

    }

    @Override
    public Product findById(String id) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Product findByName(String name) {
        // TODO Auto-generated method stub
        return null;
    }

}
Enter fullscreen mode Exit fullscreen mode

Assuming Product extends Entity, This works great!

Now as our project makes progress, we get to the part where we can finally place orders. Now, it seems like orders will also need some kind of database. Good that we have an interface already!

Almost good

So let's implement the interface.

public class OrderPersistenceServiceImpl implements PersistenceService {

    @Override
    public void save(Entity entity) {
        // TODO Auto-generated method stub

    }

    @Override
    public Order findById(String id) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Order findByName(String name) {
        // TODO Auto-generated method stub
        return null;
    }

}
Enter fullscreen mode Exit fullscreen mode

And...done? Wait a minute! Who names their orders? And that's where the problem happens. Its not that bad, we don't need it. Lets just ignore it. We will not implement the internal logic, we will not call it and if someone accidentally does, throw an exception maybe.

We don't do that here

Well..no!. If I'm using a service, I don't need to know how it does things. So nobody's going to hold me back from calling an unsupported method. Seems like my unit testing is going to take longer than expected. This is what we call interface pollution. Let's correct it now.

The alternative

Thinner interface
There is a simple way to fix this. Keep your interface thin. Now in general it would not be good to change your interface if there are classes implementing it. But here, the initial design is flawed and the interface does not define its API well. We are going to correct it by removing the findByName method. Rather, let the Product implementation handle it as its their special scenario. Here's the updated code for all three classes.

public interface PersistenceService {
    /*
     * Saves an entity to a Database
     */
    void save(Entity entity);

    /**
     * Finds an entity from database by id
     * @param entity id
     * @return entity
     */
    Entity findById(String id);

}

public class ProductPersistenceServiceImpl implements PersistenceService {

    @Override
    public void save(Entity entity) {
        // TODO Auto-generated method stub

    }

    @Override
    public Product findById(String id) {
        // TODO Auto-generated method stub
        return null;
    }

    public Product findByName(String name) {
        // TODO Auto-generated method stub
        return null;
    }

}

public class OrderPersistenceServiceImpl implements PersistenceService {

    @Override
    public void save(Entity entity) {
        // TODO Auto-generated method stub

    }

    @Override
    public Order findById(String id) {
        // TODO Auto-generated method stub
        return null;
    }

}
Enter fullscreen mode Exit fullscreen mode

And there you go! Your interface does not make false promises anymore.


Summary

  • Many small interfaces are better than few bloated interfaces.
  • Don't use interfaces if you're not going to need all their methods.
  • Should never have to use UnsupportedOperationException and never return null(that's the worst).
  • And the golden rule - Optimize early.

Discussion (0)