The interface segregation principle (ISP) is concerned with the way clients access the functionality developed in another class. It states that clients should not be forced to depend on functionality they don't use.
Following this principle has several upsides. On one hand, it protects your objects from depending on things they don't need. Removing unwanted dependencies and untangling our abstractions are some of our main goals when cleaning a software design. Another benefit is that it lets you identify the right abstractions and build bundles of cohesive behavior.
Historically another reason to follow the ISP was the advantage recompiling fewer pieces of code when the referenced classes changed. With current hardware, this is not much of an issue in most cases. The main benefit is the creation of consistent and granular abstractions.
In summary, the ISP is a tool that guides you in the creation of well-defined interfaces and abstractions.
Depending only on what you need
Robert Martin said:
Depending on something that carries baggage that you don't need can cause you troubles that you didn't expect _Robert Martin
Applying this idea to software design leads us to an obvious conclusion: an object should not depend on behavior it doesn't need. Suppose that we have a big class with lots of functionality. We will call it BigClass. Now, there are 3 other object types (ClientOne, ClientTwo and ClientThree) that use functionality from BigClass.
The three classes use only a portion of the functionality from the base class, but because they reference it, implicitly depend on a bunch of functionality they don't use. We can refactor this design to follow the ISP by creating 3 smaller interfaces that encompass only the behavior used by each of the 3 clients. This leads to a structure that looks like this:
Now, each client depends only on the functionality they need. You might think that it would be better if instead of the BigClass we had smaller classes that implement each of the 3 new interfaces, and you are right. In reality, this is not always possible, and sometimes a big class can be extremely hard to chop into smaller ones.
The ISP recognizes that sometimes these objects are needed. Instead, it tries to protect the clients by ensuring that they don't know about all the behavior in the class. Packages of related behavior used by a single client should be split into smaller and more cohesive interfaces.
Your classes show problems regarding the ISP as soon as they start to fatten up and accommodate behaviors that aren't conceptually related.
There are many sources for this fattening, a common cause is misused inheritance. This usually takes the form of subclassing from an object in the parent class to give one of the subclasses a special behavior.
Suppose you are an evil mad scientist working your latest invention: alarm robots. You want to add to your army of evil a type of robot that will patrol and notify the alarm system if it finds something weird.
You already have an EmergencyNotifier class in which you can register alarm sources of the type Alarm, and now you want to register your alarm bots.
A solution? make Robot inherit from alarm, that way you can register AlarmRobot with the EmergencyNotifier. You think that other types or Robot might benefit from being registerable in the future, so you go ahead and subclass Robot from Alarm.
Cool, now you can register the alarm robots and notifications works as expected!
This, as you imagined, is not a good solution. The original interface of Robot is now polluted with extraneous behavior. Conveniently, you can register Robots on the emergency notifier, but by inheriting from alarm your original abstraction is now broken. This change forces all the clients using these classes to depend on behavior they don't need.
There are many solutions for making this design abide by the ISP, but two are obvious and easy to implement:
- If multiple inheritance is supported, then make AlarmRobot inherit from both Robot and Alarm. I am not a big fan of multiple inheritance, so I would personally avoid this solution.
- Create an interface that embodies an Alarm abstraction and then make all Alarm-like objects in your system implement it. This way the behavior that AlarmRobot needs is segregated in multiple interfaces.
After applying the second solution our system will look like this:
Protect your clients
The takeaway lesson of this article is that you should not force clients to depend on behavior they don't need. In an ideal world, every single interface/abstraction in your code would embody the complete behavior their users need.
This is hard to achieve in reality. By different reasons, you will probably end up with a couple of big objects that do lots of things. This principle recognizes that these classes exist and urges you to create relevant abstractions that embody pieces of related behavior implemented in those classes.
To effectively apply the ISP you will need to pay attention to your system's growth and ensure that your interfaces are consistent and lean.
What to do next
- Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
- Read Uncle Bob's article this topic: ISP.
- There's more info about the OCP in chapter 10 of Clean Architecture. This and other very helpful books are in the recommended reading list.
- Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!