One of the themes that has popped up throughout our SOLID series is that of decoupling. In short, this theme argues that entities (objects, modules, functions, etc.) in a software program should be loosely coupled so as to prevent changes in one place from propagating to another. The reason this is desirable is that loosely coupled entities are easier to maintain, more flexible, and more mobile. We reviewed some of the reasons why this is the case in part 2 of the series, which covered the Open/Closed Principle, and in part 3, which covered the Liskov Substitution Principle. And yet, decoupling is so important that there is still more to say on the topic, namely, how to avoid so-called “interface pollution,” wherein classes are unnecessarily forced to implement behaviors that they don’t need. It is here that our next SOLID principle appears: the Interface Segregation Principle.
A Quick Refresher on SOLID
SOLID is an acronym for a set of five software development principles, which if followed, are intended to help developers create flexible and clean code. The five principles are:
- The Single Responsibility Principle — Classes should have a single responsibility and thus only a single reason to change.
- The Open/Closed Principle — Classes and other entities should be open for extension but closed for modification.
- The Liskov Substitution Principle — Objects should be replaceable by their subtypes.
- The Interface Segregation Principle — Interfaces should be client specific rather than general.
- The Dependency Inversion Principle — Depend on abstractions rather than concretions.
The Interface Segregation Principle
As we discussed in our review of the Open/Closed Principle, interfaces are a means of programming with abstractions rather than concretions. An interface serves as a kind of contract between two objects that interact with one another. Rather than depending directly on one another, each object instead depends on the intermediary interface. The client object (the one using another object’s behavior) doesn’t have any knowledge of how the service object (the one that implements some behavior) is structured. For its part, the service object merely guarantees that it will implement behavior described in the interface without bothering to reveal how it will do so. As a result, the two objects are effectively decoupled since neither has a direct dependency on the other. Further, interfaces allow for the creation of multiple service objects that all implement some guaranteed behavior, meaning that a client object can exploit many different behaviors depending on which service object is being used (and all without ever knowing that different kinds of service objects even exist.)
As useful as interfaces are, they raise an interesting conundrum: what happens when you want to create a service object that doesn’t actually need all of the behaviors defined on its interface? Because an interface is a contract, you would be forced to define behaviors that are effectively useless. This is known colloquially as “interface pollution” because a class may become polluted with behaviors that it doesn’t need. Worse yet, that pollution would propagate to any subclasses of a polluted superclass. This is a particularly insidious kind of coupling because it creates dependencies that don’t do anything even marginally useful.
As part of his SOLID principles, Robert C. Martin proposed a solution to this problem, which he called the Interface Segregation Principle (ISP) . Martin argued that interface pollution was primarily the result of “fat interfaces” — that is, interfaces with a large number of prescribed methods. To counter the effects of fat interfaces, Martin defined the ISP as follows:
Clients should not be forced to depend upon interfaces that they do not use.
If fat interfaces are problematic, then what’s the alternative? Martin advocates for the use of so-called “role interfaces”, which are small interfaces that only contain methods that are of interest to the objects that use them. A fat interface may therefore be broken down into smaller role interfaces that guarantee specific related behaviors. Clients that require behaviors from multiple role interfaces may simply implement each of them. Meanwhile, clients that only need limited behaviors are not forced to live with unnecessary interface pollution. In other words, separate clients can and should have separate interfaces, which in turn limits coupling and cascading breakage.
Interface Pollution in Action
Interface pollution is ultimately a question of desired behavior. If an interface exclusively defines behaviors that an implementing object actually desires, then pollution won’t be a problem. However, if an interface is broad enough as to define behaviors that won’t be used universally, then it’s probably worth investigating options to break it down. Consider the following program.
Here we have a C# program that describes the calculation abilities of two different classes:
BasicMathStudent, which uses a
AdvancedMathStudent, which uses an
AdvancedCalculator. Both types of calculators implement the
ICalculator interface, which defines the following behaviors:
SquareRoot. When either type of student is asked to
Calculate something, it uses a switch statement to identify the appropriate behavior and then uses its
Calculator member to carry out the necessary operation. (Note: For the sake of brevity, we’re setting aside a few obvious improvements like an
IMathStudent interface, a possible
Calculator class hierarchy, etc.)
The above code works just fine and both of our student types are able to carry out the required behaviors; however, if you look at the
BasicCalculator class you will see that it is polluted by having to unnecessarily implement the
SquareRoot methods. Our
BasicMathStudent is not expected to deal with exponents and thus does not need a calculator capable of those functions. This might seem innocent enough, but consider what would happen if either
BasicMathStudent had subclasses — the pollution would propagate to each of them, thus creating unnecessary dependencies. Furthermore, what if we decided that our
AdvancedCalculator should be even more capable — perhaps with a few geometry-focused methods like
Tan? We would have to add corresponding contract definitions to
ICalculator and then implement yet more unnecessary methods on
The above is of course a fairly contrived example, but it illustrates the danger of fat interfaces that require behavior that may not be necessary to all of its implementing classes.
Using Role Interfaces
In order to make our math student program ISP-adherent, we should consider breaking down the
ICalculator interface into more behavior-specific role interfaces. Leaving the rest of our program unchanged, we can do this by updating our interface definitions and only implementing them on classes that will actually use the behavior that they guarantee.
In this version, instead of an
ICalculator interface we have two role interfaces:
BasicCalculator only implements
AdvancedCalculator implements both
IExponents. Note how our
BasicCalculator is now free of pollution and yet we haven’t lost any functionality in our
AdvancedCalculator. Indeed, the program executes just as it did in the first version, except now we have fewer unnecessary couplings. Furthermore, if we wanted to add new functionality to our
AdvancedCalculator we could do so by defining new role interfaces, such as
IGeometry, and implementing them as needed. Meanwhile, our
AdvancedMathStudent classes have services that are specifically suited to their needs and expose no unnecessary behavior.
Better to start with a flexible architecture than to lock yourself in to one that is overly rigid.
Using role interfaces may seem excessive, particularly in cases that result in little pollution; however, they are an excellent way to prime your program for future changes. Better to start with a flexible architecture than to lock yourself in to one that is overly rigid.
The fourth of the SOLID principles, the Interface Segregation Principle (ISP), says that “clients should not be forced to depend upon interfaces that they do not use.” The intent of the ISP is to guard against so-called “interface pollution,” which is when an object implements an interface that guarantees behaviors beyond those needed by the particular object. Interface pollution is typically caused by “fat interfaces”, which are those that have too many behaviors defined in a single place. In order to adhere to the ISP, one should consider instead using “role interfaces”, which define limited sets of related behaviors, which can then be implemented only where they are needed. By adhering to the ISP, a program can further eliminate unnecessary coupling, thereby increasing long-term flexibility, maintainability, and mobility.
We’re approaching the end of our series on the SOLID principles! I hope you enjoyed this article on the ISP. Stay tuned for the final article, which will discuss the Dependency Inversion Principle.
If you would like alerts when a new article is published you can follow me here on Medium, on Twitter. Happy coding!