Do you know that 50% of software development time is spent on maintenance? As a result, experts dedicated their time to set principles we can apply to minimize maintenance time and refine the software design process. SOLID principles are among these principles.
SOLID principles are a set of principles set by Robert C. Martin (Uncle Bob). The main goal of these principles is to design software that is easy to maintain, test, understand, and extend.
These principles are:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion
After introducing the Liskov Substitution Principle in the previous article, in this article, we will discuss the Interface Segregation Principle (ISP) the fourth principle in SOLID.
By the end of this article, we should be familiar with the ISP definition, its violation, the relation between it and the other principles, to what extent we should apply it, how can we deal with the legacy code, and more.
Have you ever struggled to implement an interface containing methods you don’t actually need? I think we are all that person. OK, how do you deal with such a case? I think the majority of us would leave it blank or throw an unsupported exception, wouldn’t we? Don’t you believe that this is a sign of a bad design?
The ISP comes to the rescue. Robert C. Martin has defined the ISP as:
Robert C. Martin
Clients should not be forced to depend upon interfaces that they do not use.
Here, I think you might have some valid questions like:
- What is the Client, is it the client of the Client-Server architecture?
Don’t confuse this Client and any client definition you know. Simply, it is the code that’s interacting with the interface, it is the calling code. Take a look at the below example:
As you see, here the clients are
PasswordLogin classes. Here these classes are the code that interacts with the interface
In another case, if you have a third-party library that provides an interface that you have to implement. In such a case, the Client is your code, not the library.
- Is ISP specific only to Interfaces?
Definitely, NO. Actually, it could be a usual interface or an abstract class. For the sake of simplicity, I’m going to use interfaces only in this article.
Before asking the next question, I know you have others in mind, don’t worry I’ll dig into them later.
At least, after answering these questions I think it is clear to you what is the ISP goal. Yes, as you might think, the ISP states that any client code shouldn’t be forced to depend on any interface methods they don’t use. That means you should prefer a small, cohesive interface to large, fat ones which in turn, ensures single responsible, and highly cohesive software components.
Take a close look again at the previous example, don’t you feel that it has a problem, did you know it? what do you think of the
hashPassword method in the
SocialRegister class, is it relevant? That’s it, your
SocialRegister class (the client) is forced to depend on the
hashPassword method it doesn’t need, that’s the problem.
This problem is considered a violation of the Interface Segregation Principle, and if you knew about the Liskov Substitution Principle, you might think that it is a violation of it as well, which is absolutely correct.
In fact, SOLID principles are so close to each other. The ISP is particularly related to the Liskov Substitution Principle, and arguably, the ISP has the same goal as the Single Responsibility Principle. We will dig into these relations later in this article.
For now, before jumping ahead to fix the previous violation, let me first discuss what if you violate the ISP.
Have you ever worked on a legacy application and its users regularly request new features? Don’t you agree that it is more likely in that case that you would ignore the recommended design principles? For example, it is easier to add a new method to an existing interface even though it has already different responsibilities. That’s a big problem, it’s often the beginning of Interface Pollution.
Interface Pollution is adding new methods to an existing interface that your client doesn’t actually need. You can note this pollution if you have an empty implementation for an interface method or if you have to throw a UNSUPPORTED_OPERATION exception like the previous example.
Having said that, by violating the Interface Segregation Principle you would face these problems:
- A change in a large interface is going to force you to change clients that implement it even though they don’t need it.
- Large interfaces end up with more coupling, which makes your code more brittle and hard to maintain.
- Clients that depend on interfaces they don’t actually need tend to be difficult to test. As these clients are likely to be more coupled and need extra work to test methods they actually don’t need.
- As a result of this tight coupling, your code tends to be riskier to deploy.
- Violating ISP eventually ends up with a violation of other principles like SRP and LSP.
On top of that, keep in mind, that every time you’re ignoring the recommended design principles, you’re falling into the trap of Technical Debt.
Great, after knowing these problems, we shouldn’t leave our example as is, let’s know some practical ways to follow the ISP and meanwhile fix the previous example.
In fact, you have three options to apply the Interface Segregation Principle, and every option depends on the situation you have:
1- Breaking Up Large Interface
When should you choose this option?
- If you know a lot about your business requirements, and now you are building a new interface that proceeds with these requirements, instead of building a new fat interface, you should keep in mind to build small, cohesive ones.
- If you have an existing fat interface and breaking it up wouldn’t affect significantly on the codebase.
How can you apply this option?
Let’s apply this option by fixing the previous example. Here, you have the
IRegister interface, yes maybe it is small in this example but it actually lacks cohesion. Obviously, that’s because there is no password implementation in the social registration, which means the
hashPassword method in the
SocialRegister class is not relevant (not cohesive). So, let’s refactor this example by applying the ISP.
As you see, I broke up the loosely-coupled interface
IRegister into two small and cohesive interfaces
IPasswordHashing. Now, in the case of
PasswordRegister client, you need both implementations but in the case of
SocialRegister client, you just need the
IRegister implementation and not the
By applying this refactoring, your classes (clients) aren’t forced to implement methods they don’t actually need. If you want to add a new registration process, now it is up to you to apply the password implementation or not, simply, implement the
IPasswordHashing interface or leave it, on top of that, you don’t have to throw a
UNSUPPORTED_OPERATION exception to force implementing it.
2- Multiple Interface Inheritance
When should you use this option?
You should go with this option if you are forced to deal with legacy code that’s already using a fat interface (you control), and if you replace it with smaller interfaces, your code’s going to break. So, you have to keep its signature for backward compatibility. And since you can’t go and fix all those places, you can’t depend on technique number 1.
Let’s jump into an example to clarify this point:
ISendNotification interface as a fat and loosely coupled interface. And this interface is actually implemented by many classes (clients), and refactoring these classes is time-consuming and error-prone.
How can you apply this option?
Think of it, you have many places that depend on this interface, so you should keep its signature to avoid breaking the existing code, right? As a result, we should keep the old classes as is and any new classes should implement new small, cohesive interfaces instead. To do that, we can use the multiple interface inheritance.
As you see, now, the
Old class keeps implementing the
ISendNotification to avoid breaking any existing code. Meanwhile, any new classes like
New2 would implement the new broken interfaces. I broke up the loosely-coupled interface
ISendNotification into smaller interfaces
ISendSmsNotification and emptied its body to keep just its signature. Thanks to the multiple interface inheritance that enabled us to do this technique.
3- The Adapter Design Pattern
When should you use this option?
You should use this technique if you are forced to use a fat interface that you don’t have control over, like interfaces coming from a third-party library or SDK. Of course, in such a case the previous techniques wouldn’t work.
How can you apply this option?
Since you can’t rid of this fat interface, you should create small, cohesive interfaces that follow the adapter design pattern, and your code should work with these interfaces that you control, and they, in turn, can work with the original fat interface. Meanwhile, only the adapter should know about the underlying fat interface.
Imagine that the
IThirdPartyService is coming from a third-party library and you have no control over it. So you can’t break it up. To work around this issue:
- you can adapt this fat interface to work in your code via the adapter
ThridPartyServiceAdapterwhich is the only part of your code that should know about this fat interface.
- Afterward, you can create small, cohesive interfaces
IPromotionsServicethat could be implemented directly in your code instead of the fat interface
- On top of that, you can use the adapter
ThridPartyServiceAdapterin your classes using the Dependency Injection, which is a perfect solution in such a case.
Having said that, now, your code is using the third-party library through an under-control adapter, and at the same time doesn’t know anything about the third-party library itself.
Sounds good, but how can you detect that your code has problems, let’s discuss some code smells that indicate that your code is violating the ISP.
Large interfaces are harder to fully implement and, thus, more likely to only be partially implemented. As a result, you are most likely to violate the ISP.
You might argue about the Large keyword. Maybe you’re right, it might be vague, but here I am talking about the probability of violation. So to have the ideal formula, you can consider both the Largeness and Cohesion of the interface.
Take a look at the first example, clearly the
IRegister interface is not large but less cohesive which in turn leads to the ISP violation.
What is the easiest implementation you will do for a large interface you don’t need? Yes, to throw a
Look at the first example
SocialRegister class, it actually doesn’t need to implement the
hashPassword method, since it isn’t relevant to social registration. So, to work around this issue, the
hashPassword method throws a
UNSUPPORTED_OPERATION exception which in turn violates the ISP.
You might choose to leave a not-needed method blank instead of throwing an exception. That also violates the ISP.
After reaching out at this point, you might end up thinking: Great, there is no need for many-methods interfaces, all I need are just single-method interfaces to avoid the ISP violation completely.
If you think twice about this conclusion, you could end up with a bunch of single-method interfaces that are much more difficult to use, group together, and understand as your code would be very complicated especially if it was a large codebase.
So, what should you do instead? There are two options:
- If your business requirements are clear and you know them exactly upfront, you should consider Cohesion and Largeness when you’re building your interfaces. The more experience you have, the more accurate you reach the balance point.
- If you’re building a project from the beginning and you don’t know the entire scope of it, you should consider the (PDD) Pain-Driven Development principle. Don’t you remember the PDD principle we explained in the Single Responsibility Principle article?
Simply put, you should build your interfaces as simple and cohesive as you can. You shouldn’t break up interfaces or use the adapter design pattern just to satisfy the ISP but, rather, look for places in your code where the app is painful to work with as your app grows, then, see if you can apply the ISP here to improve your design and mitigate this pain.
Let’s move forward to some interesting points. I stated that ISP is closely related to the other SOLID principles. Let’s discuss its relation to LSP and SRP.
Let’s recall what is the primary goal of the Liskov Substitution Principle, the LSP brings additional constraints to the Object-Oriented Design and states that these relationships (Inheritance or Composition) aren’t sufficient and should be replaced with IS-SUBSTITUTABLE-FOR. That means the primary goal of LSP is to substitute between a SuperType and a SubType without breaking the existing code.
Great, by knowing that, let’s return to our first example, I said that this example is violating the LSP, but why?
Yes, as you might’ve guessed, the
SocialRegister class (the SubType) throws an
UNSUPPORTED_OPERATION exception and partially implements the
IRegister interface (the SuperType). In other words, the SuperType
IRegister and SubType
SocialRegister are not substitutable, which violates the LSP.
But how can you solve this LSP violation in that case? The short answer is by applying the Interface Segregation Principle.
As a result, in my opinion, the Interface Segregation Principle is just a technique for correctly applying the Liskov Substitution Principle.
But what about the Single Responsibility Principle?
In his article, Mark Seemann states that:
Although I do understand the subtle differences between SRP and ISP I think they are so closely related that one of them is really redundant. We can remove the ISP and still have a fairly good acronym: SOLD (although SOLID is still better).
To know why he said that, let’s compare the two principles from two perspectives, the Context, and the Goal of both.
- Context Perspective
You might argue: This is aggressive from Mark Seemann. There is a clear difference between the two principles, the SRP is concerned with classes while the ISP is concerned with interfaces.
In my opinion, that’s not correct. What will you do if you use an abstract class instead of an interface? In that case, are you going to ignore the ISP?
For me, SOLID principles are more mindset than just guides I have to follow. I think Single Responsibility should be applied to both classes and interfaces. Meanwhile, Interface Segregation isn’t exclusive to just interfaces, but classes as well.
So, I think the context of the two principles is the same.
- Goal Perspective
Let’s recall what is the goal of the Single Responsibility Principle, your classes and interfaces should have one responsibility and one reason to change, which means SRP encourages you to prefer small, cohesive components to large, loosely coupled ones.
Think of an example, in which you violated the ISP by defining a fat interface that has multiple methods and these methods aren’t cohesive and not related to each other. It is clear that implementing this interface would eventually lead to the Single Responsibility Principle violation as well, isn’t it? So, I think the goal of the two principles is the same as well.
Having said that, could you tell me the difference between SRP and ISP? For me, there is a blurred line between Single Responsibility and Interface Segregation Principles. It is better to say, ISP is a small part of SRP or ISP is just a technique to correctly apply the SRP.
After reaching out at this point, what do you think, it is better to be SOLID or SOLD as per Mark’s saying?
I know what is on your mind, No, I’m not contradictory, I am not against the ISP, rather I am against making it a standalone principle to make a catchy and fancy SOLID acronym. As I said, I think of Interface Segregation Principle as a technique to correctly apply the other SOLID principles.
But if you think of it this way, then it actually has some benefits:
- No fat interfaces with multiple responsibilities.
- Your implementing classes would be concerned only with the methods they indeed need.
- Your code would be more maintainable, testable, and extendable.
- Eventually, avoiding violating other principles like LSP and SRP.
In this article, we have learned the fourth principle in SOLID which is Interface Segregation Principle.
We learned that you should prefer the small, cohesive interfaces to the large, loosely-coupled ones.
Then, we knew some practical ways we can follow to correctly apply the ISP like breaking up the large interfaces, multiple interface inheritance, or using the adapter design principle.
Afterward, we learned about some code smells that might indicate your code is violating the ISP.
Finally, we learned about the relationship between this principle and other principles like SRP and LSP.
- SOLID Design Principles Explained: Interface Segregation with Code Examples
- Interface Segregation Principle in Java
- Interface Segregation Principle: Everything You Need to Know
- SOLID Principles : The Interface Segregation Principle
- The Interface Segregation Principle
- SOLID Principles for C# Developers
If you liked this article please rate and share it to spread the word, really, that encourages me a lot to create more content like this.
If you found this article useful, check out these articles as well:
- Strategy vs State vs Template Design Patterns
- MongoDB GridFS, Made Simple
Thanks a lot for staying with me up till this point. I hope you enjoy reading this article.