The Interface Segregation Principle (ISP) is the fourth principle of SOLID, and it has a simple but powerful goal: to make your software more modular and easier to maintain by keeping interfaces lean and focused.
The formal definition states: “Clients should not be forced to depend on methods they do not use.”
In this context, a “client” is any class that implements an interface. If a class is forced to implement methods it has no use for, it leads to bloated code, confusion, and potential bugs. ISP guides us to solve this by breaking down large, “fat” interfaces into smaller, more specific ones.
Let’s use a real-world analogy. Imagine a company that makes printers. They offer a basic model that only prints and an advanced all-in-one model that can print, scan, and fax.
What if they used the same user interface for both?
graph TD
subgraph "Problem: One 'Fat' Interface"
UI("User Interface App")
UI -->|Print()| BasicPrinter("Basic Printer")
UI -->|Scan()| BasicPrinter
UI -->|Fax()| BasicPrinter
style BasicPrinter fill:#f9f,stroke:#333,stroke-width:2px
end
subgraph "Solution: Segregated Interfaces"
BasicUI("Basic UI") --> |Print()| CleanBasicPrinter("Basic Printer")
AdvancedUI("Advanced UI") --> |Print(), Scan(), Fax()| AllInOnePrinter("All-in-One Printer")
style CleanBasicPrinter fill:#9cf,stroke:#333,stroke-width:2px
style AllInOnePrinter fill:#9cf,stroke:#333,stroke-width:2px
end
linkStyle 1 stroke:red,stroke-width:2px,stroke-dasharray: 5 5
linkStyle 2 stroke:red,stroke-width:2px,stroke-dasharray: 5 5
The user of the basic printer would see buttons for “Scan” and “Fax” that do nothing. This is a poor user experience and, in software terms, a violation of ISP. The BasicPrinter client is being forced to acknowledge an interface with methods (scan, fax) it cannot fulfill.
The Problem: A “Fat” User Management Interface
Let’s translate this into a code example. Imagine we’re building a user management system. We have two types of users:
RegularUser: Can only view content.AdminUser: Can view content, add new users, and remove users.
A naive first approach might be to create a single, all-encompassing IUserActions interface.
// A "fat" interface that violates ISP
public interface IUserActions {
void viewContent();
void addUser(String username);
void removeUser(String username);
}
Now, when our AdminUser class implements this, everything works perfectly because an admin can perform all these actions.
public class AdminUser implements IUserActions {
@Override
public void viewContent() {
System.out.println("Admin viewing content...");
}
@Override
public void addUser(String username) {
System.out.println("Admin adding user: " + username);
}
@Override
public void removeUser(String username) {
System.out.println("Admin removing user: " + username);
}
}
However, the problem becomes obvious when RegularUser implements the same interface.
public class RegularUser implements IUserActions {
@Override
public void viewContent() {
System.out.println("Regular user viewing content...");
}
// This class is forced to implement methods it doesn't need!
@Override
public void addUser(String username) {
throw new UnsupportedOperationException("Regular users cannot add users.");
}
@Override
public void removeUser(String username) {
throw new UnsupportedOperationException("Regular users cannot remove users.");
}
}
[!WARNING] Throwing
UnsupportedOperationExceptionis a major red flag. It indicates that your abstraction is flawed. This not only violates ISP but also the Liskov Substitution Principle (LSP), because you can no longer safely substitute the base type (IUserActions) with a subtype (RegularUser) without risking runtime errors.
The Solution: Segregate the Interface
The solution, as prescribed by ISP, is to break our “fat” interface into smaller, role-specific interfaces.
mindmap
root("IUserActions (Fat Interface)")
"viewContent()"
"addUser()"
"removeUser()"
"("Segregated Interfaces")"
"ContentViewer"
"viewContent()"
"UserAdministrator"
"addUser()"
"removeUser()"
Here’s what the new, segregated interfaces look like in code:
// Interface for any user that can view content
public interface IContentViewer {
void viewContent();
}
// Interface for any user that can manage other users
public interface IUserAdministrator {
void addUser(String username);
void removeUser(String username);
}
With these smaller interfaces, our classes can now implement only the functionalities they truly possess.
Refactoring the RegularUser
The RegularUser class becomes much cleaner. It only cares about viewing content, so it only implements IContentViewer.
- public class RegularUser implements IUserActions {
+ public class RegularUser implements IContentViewer {
@Override
public void viewContent() {
System.out.println("Regular user viewing content...");
}
-
- @Override
- public void addUser(String username) {
- throw new UnsupportedOperationException("Regular users cannot add users.");
- }
-
- @Override
- public void removeUser(String username) {
- throw new UnsupportedOperationException("Regular users cannot remove users.");
- }
}
No more exception-throwing methods! The class is now simple, honest, and focused on its single responsibility.
Refactoring the AdminUser
The AdminUser needs to perform all actions, so it simply implements both interfaces. This is perfectly valid and is the intended way to use segregated interfaces.
- public class AdminUser implements IUserActions {
+ public class AdminUser implements IContentViewer, IUserAdministrator {
@Override
public void viewContent() {
System.out.println("Admin viewing content...");
}
@Override
public void addUser(String username) {
System.out.println("Admin adding user: " + username);
}
@Override
public void removeUser(String username) {
System.out.println("Admin removing user: " + username);
}
}
The final class structure is clean, logical, and robust.
classDiagram
direction LR
class IContentViewer {
<<Interface>>
+viewContent()
}
class IUserAdministrator {
<<Interface>>
+addUser(String)
+removeUser(String)
}
class RegularUser {
+viewContent()
}
class AdminUser {
+viewContent()
+addUser(String)
+removeUser(String)
}
RegularUser --|> IContentViewer
AdminUser --|> IContentViewer
AdminUser --|> IUserAdministrator
Best Practices and Key Takeaways
[!TIP] Think from the Client’s Perspective: When designing an interface, always consider the classes that will implement it. Ask yourself, “Will my client need all of these methods?” If the answer is no, your interface is likely too “fat.”
- Favor Role-Based Interfaces: Create interfaces based on the roles objects play in your system (e.g.,
IContentViewer,IUserAdministrator,IPaymentProcessor). - Smaller is Better: Don’t be afraid to create many small, focused interfaces. It’s better to have many small interfaces than one large, unmaintainable one.
- ISP Prevents Downstream Issues: Adhering to ISP often prevents violations of other principles, especially the Single Responsibility Principle and the Liskov Substitution Principle.
By applying the Interface Segregation Principle, you create systems that are more decoupled, easier to refactor, and simpler to understand and test.
🧠 Quiz: Test Your Knowledge
Question: You are designing an application for a library. You have an interface `ILibraryItem` with methods `checkOut()`, `returnItem()`, `payFine()`, and `renewOnline()`. However, reference books (`ReferenceBook` class) can neither be checked out nor renewed online. Which of the following is the best way to apply ISP?
- Implement all methods in `ReferenceBook` but have `checkOut()` and `renewOnline()` do nothing or throw an exception.
- Create a single, smaller interface `IBorrowable` with `checkOut()`, `returnItem()`, and `renewOnline()`. Have another interface `IFinePayable` for `payFine()`. A normal book would implement both, while a reference book would implement neither.
- Break the interface into `IBorrowable` (`checkOut`, `returnItem`), `IRenewable` (`renewOnline`), and `IFinePayable` (`payFine`). A normal book implements all three, while a `ReferenceBook` might not implement any of them, depending on whether fines can be applied to it.
- Use a base class instead of an interface.
Show Answer
Correct Answer: C. This is the most granular and flexible approach. It breaks down behaviors into their smallest logical units, allowing classes to pick and choose exactly the functionalities they support. Option B is good, but `IRenewable` should likely be separate from `IBorrowable`, as not all borrowable items may be renewable.