The Interface Segregation Principle (ISP) is one of the five essential SOLID design principles. These principles are guidelines for the proper usage of object-oriented features. The principle ISP states that:
Client should not be forced to depend on methods it does not use.
It is all about interface, the common abstractions available in most OOP languages such as C#, VB.NET or Java. A more complete and actionable explanation of ISP is:
ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces.
SOLID Principles Summary
Before delving into the Interface Segregation Principle, let’s take a quick look at how it stands in the SOLID design principles:
- The Single Responsibility Principle (SRP): A class should have one reason to change. This principle is about how to partition your logic into classes and avoid ending up with some monster classes (known as god class).
- The Open-Close Principle (OCP): Modules should be open for extension and closed for modification. To implement a new feature, it is better to add a new derived class instead of having to modify some existing code.
- The Liskov Substitution Principle (LSP): Methods that use references to base classes must be able to use objects of derived classes without knowing it. Array implements IList<T> but throws NotSupportException on IList<T>.Add(). This is a clear LSP violation.
- The Interface Segregation Principle (ISP): The client should not depend by design on methods it does not use. This is why designers introduced interfaces like IReadOnlyCollection<T>. Often the client just needs a subset of features like read-only access instead of full read-write access.
- The Dependency Inversion Principle (DIP): Depend on abstractions, not on implementations. Interfaces are much less subject to changes than classes, especially the ones that abide by the (ISP) and the (LSP).
C# Example of the Interface Segregation Principle
Since the introduction of generic in .NET and C#, developers are consuming the interface IList<T>. With methods like Add(T), Contains(T), IndexOf(T), and the indexer syntax list[index] this interface represent all sorts of list of items. But often a client only uses a read-only version of a list. In all these situations methods like Add(T) and Remove(T) are not necessary. This is a clear violation of the ISP: The client should not be forced to depend on methods it does not use.
This is why the interface IReadOnlyList<T> was introduced later with .NET 4.5. This ISP violation became prominent. The .NET developers needed a finer-grained interface to avoid depending on Add(T) and Remove(T) methods they didn’t need. IList<T> needed to be segregated to address this need.
Why is this a problem?
Being coupled with some unneeded behavior is a problem:
- In the optimal scenario, it is a waste. This forces the client to consume precious brain-cycles to consider something he/she doesn’t need.
- In the worst-case scenario, it introduces the risk of errors. Clients may inadvertently misuse additional functionalities, such as attempting to add an element to an array using ICollection<T>.Add().
When an old ISP violation prevents good design forever
It would make sense that IList<T> inherits from IReadOnlyList<T>. A list of items is a read-only list with additional operations like Add(T) and Remove(T). Unfortunately, this is not the case. Since IReadOnlyList<T> appeared later this would have caused some backward compatibility issues.
For example, this class would have been broken if IList<T> was implementing IReadOnlyList<T>. In that case, the property Count would have been defined by IReadOnlyList<T>. And the explicit interface implementation IList<T>.Count would break:
1 2 3 4 |
class MyList<T> : IList<T> { int IList<T>.Count { get { return 0; } } // ... } |
A small interface is not necessarily a good abstraction
A single-method interface often makes sense:
- an IExecutor that Execute(),
- an IVisitor that Visit(),
- an IParent that exposes Children { get; }.
Often, such a minimalist interface should be generic. For example, the interface ICloneable available since the inception of .NET is nowadays considered a code smell. When using it the client needs to downcast the cloned Object reference returned to do anything useful with the cloned instance.
1 2 3 |
public interface ICloneable { object Clone(); } |
ICloneable has another major drawback: it doesn’t inform the client if the clone operation is deep or shallow. This problem is even more serious than the Object reference downcasting one: it is a real design problem. As we can see a minimalist interface is not necessarily a good abstraction. In this example, the lack of information means ambiguity for the client. This would have been a better design:
1 2 3 4 5 6 |
public interface IDeepCloneable<T> { T DeepClone(); } public interface IShallowCloneable<T> { T ShallowClone(); } |
A fat interface is not necessarily a design flaw
A static analysis rule like Avoid too large interfaces can certainly pinpoint most of the ISP violations. A threshold of 10 methods is proposed by default to define what a too-large interface is.
However, as always with code metrics and static analysis, such a rule can also spit some false positives. For example, the .NET interface IConvertible is fat but is valid. Classes that implement it should be convertible to all those primitive types.
For example, we can imagine a Complex number class that implements IConvertible. All to-number methods would return the magnitude of the complex number. ToBoolean() would return false only if the complex is zero. ToString() would return the complex string representation.
ISP and the Liskov Substitution Principle (LSP)
ISP and LSP can be likened to two sides of the same coin:
- ISP is the client perspective: If an interface is too fat probably the client sees some behaviors it doesn’t need.
- LSP is the implementer perspective: If an interface is too fat probably a class that implements it won’t implement all its behaviors. Some behavior will end up throwing something like a NotSupportedException.
Remember the ICollection<T> interface already discussed in the LSP article. This interface forces all its implementers to implement an Add() method. From the Array class perspective, implementing ICollection<T> is a violation of the LSP because the array doesn’t support element adding:
In the same way, many clients will only need a read-only view of consumed collections. ICollection<T> also violates the ISP: it forces those clients to be coupled with Add() / Insert() / Remove() methods they don’t need. The introduction of IReadOnlyCollection<T> solved both ISP and LSP violations.
This example also shows that ISP doesn’t necessarily mean that a class should implement several lightweight interfaces. It is fine to nest interfaces like russian-nesting-dolls. ICollection<T> is a bit fat, it does a lot, read, add, insert, remove, count… But this interface is well-adapted both for classes that are read/write collections and for clients that work with read/write collection. It makes more sense to nest both read/write behaviors into ICollection<T> than to decompose both behaviors into IReadOnlyCollection<T> and a hypothetical IWriteOnlyCollection<T> interface.
Conclusion
Embracing the Interface Segregation Principle (ISP) in C# is a pivotal step toward achieving robust and maintainable software design. ISP is about preventing inadvertent coupling between a client code and some behaviors it won’t need. As for all SOLID principles, ISP is better applied if you write tests to exercise your code. ISP is about the client perspective and writing tests transforms you for a while into a client of your code.
Through the examples explored in this article, we’ve witnessed how ISP promote a client-centric approach, preventing clients from being burdened with functionalities they don’t require. As we incorporate ISP into our C# projects, we not only enhance code clarity but also foster a more resilient and adaptable architecture. As we continue to prioritize clean and modular code, ISP stands as a guiding principle for achieving excellence in C# software design.
Good way of explaining, and nice article to get information regarding
my presentation focus, which i am going to deliver in school.