The Interface Segregation Principle represents the “I” in SOLID and states that no client should be forced to depend on methods it does not use. The easiest way to achieve compliance is to to split large interfaces which are not specific to a single client into smaller, more specific ones and let the clients choose which ones to depend on.
Additionally, segregating interfaces ties well into the Single Responsibility Principle, as well as Separation of Concerns.
Liskov Substitution vs Interface Segregation
The Interface Segregation and Liskov Substitution principles are related to each other, in that breaking the earlier may lead to breaking the latter. When an interface is bloated, it may be difficult to implement classes to suit specific needs. This tempts developers to create workarounds, such as throwing a NotImplementedException() on methods which are not needed. The moment a class breaks the contract of behavior defined in its parent, that’s when an ISP violation turns into an LSP violation.
Example
How it’s broken
Suppose you were tasked with managing a fleet of spaceships. Some of them will do battle with the enemy, while others will be sent to research new technologies. You create an interface:
public interface Spaceship { String fly(); String land(); String shoot(); String doScience(); }
With the interface in hand, you design two different spaceships. One for fighting:
public class LightCruiser implements Spaceship { @Override public String fly() { return "Flying to space!"; } @Override public String land() { return "Landing..."; } @Override public String shoot() { return "Bang! Bang! Lasers!"; } @Override public String doScience() { return "Magnets... How do they work?"; } }
And another for research:
public class ScienceVessel implements Spaceship { @Override public String fly() { return "Flying scientifically!"; } @Override public String land() { return "Landing cautiously..."; } @Override public String shoot() { return "Help! I'm unarmed!"; } @Override public String doScience() { return "Gathering data on nearest neutron star..."; } }
The spaceships could be commanded like so:
LightCruiser lightCruiser = new LightCruiser(); System.out.println(lightCruiser.fly()); System.out.println(lightCruiser.land()); System.out.println(lightCruiser.shoot()); ScienceVessel scienceVessel = new ScienceVessel(); System.out.println(scienceVessel.fly()); System.out.println(scienceVessel.land()); System.out.println(scienceVessel.doScience());
You might notice the Liskov Substition Principle violation in the implementations of the Spaceship interface. A LightCruiser cannot do any actual science, while a ScienceVessel cannot shoot. This is unacceptable! Alas, it’s not the only problem.
The Interface Segregation Principle violation lies in the fact that at least one of the methods in Spaceship is unused by at least one its implementations (in this case both implementations have unused methods). In the case of ScienceVessel, the unused method is shoot(). In the case of LightCruiser, the unused method is doScience(). This has additional consequences when we consider the backwards force applied by clients upon interfaces, as described by Robert C. Martin in his June 1996 paper for The C++ Report called “The Interface Segregation Principle“. It can be observed when the user forces a change to the interface.
As an example of this, consider that shooting at a specific enemy is now considered to be important functionality and blind firing is no longer allowed for LightCruisers. Therefore, you have decided to modify the Spaceship interface as follows:
public interface Spaceship { String fly(); String land(); // String shoot(); <- This method was removed String shootAt(String enemy); // This method was added String doScience(); }
The user (LightCruiser) has forced a change to the Spaceship interface. The ramification of this is that the LightCruiser will have to be modified to accommodate the changes. This is fine and expected. However, the ScienceVessel will have to be modified as well, even though it has nothing to do with battling!
How to fix it
A possible way to fix this is to extract the shootAt and doScience methods to their own interfaces. The shootAt method, in our case, belongs to Warships (of which LightCruiser is a subtype), while doScience is the domain of ResearchVessels (of which ScienceVessel is a subtype).
public interface Spaceship { String fly(); String land(); // Removed unnecessary methods }
public interface Warship { String shootAt(String enemy); }
public interface ResearchVessel { String doScience(); }
The specific spacecraft can now use multiple inheritance (by interface) to implement the interfaces they need (and nothing more):
public class LightCruiser implements Spaceship, Warship { @Override public String fly() { return "Flying to space!"; } @Override public String land() { return "Landing..."; } @Override public String shootAt(String enemy) { return "Shooting at " + enemy; } }
public class ScienceVessel implements Spaceship, ResearchVessel { @Override public String fly() { return "Flying scientifically!"; } @Override public String land() { return "Landing cautiously..."; } @Override public String doScience() { return "Gathering data on nearest neutron star..."; } }
An encouraged refactoring in this case would be to make both Warship and ResearchVessel extend Spaceship – that way the spacecraft would only need to declare an implementation of a single interface. This makes sense because bothWarship and ResearchVessel are a type of Spaceship. It may not always make logical sense in other scenarios – for example, if the Warship interface was instead called WarVehicle, forcing the fly() and land() methods onto it would be ill advised.
In the end, though, we are left with neatly segregated interfaces, which even made some implicit domain logic explicit (the fact that we are managing warships and research vessels and that they have distinct functionality). Modifying combat features will not force us to modify non-combat vehicles and modifying research capabilities will not force changes to war machines. Additionally, we are no longer forced to violate other principles, such as the Liskov Substitution Principle.
ISP violations in clients
It is important to note that an ISP violation does not only occur in interface implementations. If service methods are not properly encapsulated into cohesive interfaces, it creates a possibility that a change to one of the clients will force a change to the service’s fat interface, therefore potentially causing a change to every other class that depends on that service.
Conclusion
An interface which is not cohesive should be split into smaller ones so that each can serve a different set of clients.
Fat interfaces can lead to violating the Liskov Substitution Principle, but may also force changes to classes which do not use the methods being changed. Furthermore, there is a possibility that an interface like that is hiding important domain knowledge.
Make sure that your interfaces are specific and that all clients will always want to use its methods.
Photo by Pixabay
Be First to Comment