This blog is part of a series explaining the SOLID design principles by looking at code from real projects or frameworks. Last time, we looked at the Dependency Inversion Principle. This time, we’ll discuss the last principle: The Interface Segregation Principle.
SOLID is an acronym for the following design principles:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency-Inversion Principle
The SOLID principles are often explained by using simple examples, but that sometimes makes it hard to spot them in your own code for a real project. That’s why I searched for some examples of these issues in Open Source projects and use them in this series.
The Interface Segregation Principle
The Interface Segregation Principle was defined by Robert C. Martin and states:
Clients should not be forced to depend on methods they do not use.
This principle deals with the problems of big interfaces that are used by different clients with different needs. Used by can mean by using inheritance (when the client derives from it) or it uses composition (aggregate). This means the methods of an interface can be broken up into multiple groups, where each group serves a different client. In other words, one group is there for one client, while the other group is there for another one.
If you have these big interfaces that serves different clients with different needs, all clients depend or is forced to use the entire interface even when it is not using it. This can lead to a couple of things. First, the client is coupled to methods that is doesn’t need but will have an effect if it changes due to new requirements or interactions with other clients. The client is then forced to also change, increasing the blast radius of a change and possibly create side effects. The second problem is that a client perhaps can’t create a sensible implementation of the interface for each method, possibly leading to a violation of the Liskov Substitution Principle! In fact, if you have a violation of the Liskov Substitution Principle, it could very well be that you also have a violation of the Interface Segregation Principle!
The Interface Segregation Principle states that you should have different, smaller and specific interfaces for each (set of) clients. The clients then only work with the interface that is applicable for them and therefore is only dependent on that part. Clients don’t need to know anything about each other and don’t interfere when changes to the interface need to happen.
An example violation in the wild
For this principle, we’ll take the same example as that of the Liskov Substitution Principle. In this project, you’ll find these classes (simplified to focus on the violation):
With the code looking like this:
What’s good to note is that while the UML diagram only shows 1 Property called Layout, the property actually consists of two parts: the getter and the setter of the property. Because NLogViewerTarget is derived from TargetWithLayout, it must implement both the getter and the setter. But NLogViewerTarget doesn’t use or in fact doesn’t want to have a setter. But it is forced to depend on methods (in this case the setter of the property Layout) that it doesn’t use!
They worked around it by ignoring the value passed to the setter, and thereby violating the Liskov Substitution Principle! It would be better to have a separate interface for setting the Layout (for example IConfigurableLayout) and let it be implemented only by those layouts that wants to be able to set it.
How to spot these kinds of violations
There are different ways that you can use to find these kinds of violations. Look for interfaces with more than a handful of methods. Ask yourself: do these methods form a cohesive group and are they all used by all clients? If not, it is probably a violation of the principle.
It also helps when you search for designs that have multiple layers of inheritance and use base classes or abstract classes. The example used above also uses it. When you use too much inheritance, it often becomes harder and harder to create sensible abstractions that work for all specific clients.
Another way to end up in this situation is when you use the “Extract interface” option of IDEs on an existing class and then derive a new class from it. By extracting an interface from an existing class, you could also end up with one big interface instead of multiple smaller ones.
Figure 1 Extract Interface functionality in IntelliJ
The last method use can use is looking at the names of your interfaces. If they don’t have names that contain …able (e.g. Printable, Settable, Configurable, Disposable) and almost have the same name as the class that implements it, it could also be an interface that is too big.
How to solve these violations
Always think whether you created the right abstraction and structure. If you are using multiple layers of inheritance, try using composition instead of inheritance.
Try to split up your interface into multiple, smaller interfaces that define a specific need or role (e.g. Printable, Settable, Configurable, Disposable) and implement it only where it is needed. You could use multiple inheritance to make a class implement more than one interface. Martin Fowler calls these interfaces Role Interfaces. If multiple inheritance doesn’t work, you could look at the Adapter pattern where the original class implements one interface and Adapter class implements another one.
As you can see, a violation of the Interface Segregation Principle can lead to a violation of the Liskov Substitution Principle. So, when you find the latter, be suspicious! Also, when, in order to make some changes, you also have to change unrelated parts as well, it could be a sign of this problem. Having small specific interfaces allow changes to by more localized and reduces the chance of regression in other parts of the code.
This was the last post of this series! I hope it was useful and that it helped you to spot these principles in your own code!
This blog was written by Harm Pauw.