In this series we are looking at the SOLID design principles. In this week’s tip, we’ll cover the fourth principle in the acronym: the Interface Segregation principle (LSP).
If you missed the previous posts in this sequence, you can find them here:
- SOLID Design Principles Part 1 – Single Responsibility Principle
- SOLID Design Principles Part 2 – Open-Closed Principle
- SOLID Design Principles Part 3 – Liskov Substitution Principle
The Interface Segregation Principle
“Many small specific interfaces are better than one large general-purpose interface”.
The dependency of one class to another should depend on the smallest possible interface. The purpose is to make clients use the smallest and most cohesive interface as possible.
Most clients of a class only need a small number of the methods of an interface. To facilitate this, we should create separate, focused interfaces, each of which serves a specific purpose. A class will then implement only the interfaces it needs, as opposed to having to implement many methods that it doesn’t need. Implementing a large general-purpose interface often leads to accidental close class couplings and additional dependencies.
Extending the ISP to its logical conclusion, the most specific and focused interface will consist of only one single method to implement. This is often called a SAM (Single Abstract Method) interface. In the Java API, it is now called a functional interface. A functional interface has a number of additional uses with lambdas in functional programming. See my previous blog post on functional interfaces for more information.
Java API Examples
There are a large number of obvious examples of the ISP in the standard Java API. Just in the java.lang package we can find the Appendable, AutoCloseable, CharSequence, Comparable, Iterable, Readable and Runnable interfaces. All of these interfaces have very focused, narrow uses. Most are functional interfaces containing only a single abstract method.
Two of the previous interfaces are not functional interfaces, but they are still highly focused on specific use cases. Appendable has three overloaded append() methods to append characters to the end of other objects. CharSequence defines six common methods that provide read-only access to objects that contain sequences of chars.
Custom Interface Example
Let’s look at how we would implement ISP in our own design. Let’s say we’re modelling an Employee class and a Contractor class. Obviously they have similar fields: name, surname, dateOfBirth, gender, etc. They have differences too: employees receive a salary, while contractors have an hourly rate that determines their remuneration. They have similar methods: work(), doTimeSheets(), drinkCoffee(), etc. The common fields and methods can be factored out and moved to a Person base class. The work() and doTimeSheets() methods obviously can’t be moved to the Person class because they have nothing to do with a Person.
A good idea would be to move the work() and doTimeSheets() methods into an interface that would be appropriately implemented in the Employee and Contractor classes.
public interface Productive {
public void work();
public void doTimeSheets();
}
We should debate whether doing time sheets is actually being productive, and whether the method belongs in the Productive interface. Let’s think of a Gardener or a Guard class that inherits from Employee and implements Productive. No one can argue that they work, but no one would expect them to fill in time sheets.
We might also want to model a GuardDog to patrol with the Guard. Again, no one would argue with the fact that the GuardDog is a working dog, and should have a work() method. And very importantly , a GuardDog would never fill in a time sheet!
Obviously putting the doTimeSheets() method in the Productive interface limits its use, and doesn’t model Productive objects correctly. Doing so could lead to unintended class dependencies.
What now? ISP to the rescue! Let’s apply the Interface Segregation principle to our current design and split the Productive interface into two focused interfaces, each with their own use cases:
public interface Productive {
public void work();
}
public interface CompleteAdminstrativeTasks {
public void doTimeSheets();
}
This would lead to code which follows good design principles by conforming to the ISP:
public class Employee extends Person implements Productive, CompleteAdminstrativeTasks {
public void work() {}
// appropriate code here...
}
public void doTimeSheets() {}
// appropriate code here...
}
}
public class GuardDog extends Dog implements Productive {
public void work() {}
// appropriate code here...
}
}
What’s next?
Next week we’ll look at the final principle in the acronym: the Dependency Inversion Principle (DIP).
As always, please share your comments and questions.