[OOP] Five Principles of Object Orientation (SOLID) - Interface Segregation Principle (ISP)
[OOP] Five Principles of Object Orientation (SOLID) - Interface Segregation Principle (ISP)
The Interface Segregation Principle states that an object should not depend on methods it does not use.
It recommends inheriting/implementing only the methods that are truly necessary, in order to prevent implementing meaningless methods on an object. If the object to be inherited is too large in scope, it's better to split its methods into smaller interfaces.
The diagram above illustrates the problem that arises when inheriting from an object that is too large, and how splitting it into interfaces solves the problem.
When the objects on the left and right each inherit from the object in the middle, the object on the left has no issue since it needs all of the implemented methods. The object on the right, however, doesn't need anything besides Method1. But because it inherited everything, it's forced to carry those methods whether it likes it or not — or in the worst case, it even has to implement methods it doesn't need.
But let's instead split the methods of the object to be inherited into interfaces grouped by behavior. Then each object only needs to inherit the interface it actually needs, so each one ends up with only the methods it requires. This is what the Interface Segregation Principle aims for.
Let's use an example to see the difference in code between not following the Interface Segregation Principle and following it.
For example, suppose there is an object representing a smartphone. Since this smartphone object is a relatively recent model, it includes a variety of features beyond the typical smartphone functionality, such as wireless charging, an AR viewer, and biometric authentication.
If we implement the S20 using this, all of the smartphone object's behaviors are needed, so it satisfies ISP. However, if we implement the S2, it doesn't provide features like wireless charging or biometric authentication. Yet, because the parent smartphone object includes these interfaces, the S2 ends up having to implement functionality it doesn't even need — a wasted effort.
JAVA
/** * Abstract smartphone object * * @author RWB * @since 2021.08.16 Mon 16:48:03 */ abstract public class SmartPhone { /** * Call function * * @param number: [String] phone number */ public void call(String number) { System.out.println("Connecting call to " + number); } /** * Text message sending function * * @param number: [String] phone number * @param text: [String] message content */ public void message(String number, String text) { System.out.println(number + ": " + text); } /** * Wireless charging function */ public void wirelessCharge() { System.out.println("Wireless charging"); } /** * AR function */ public void ar() { System.out.println("AR feature"); } /** * Abstract biometrics function */ abstract public void biometrics(); }
Above is the SmartPhone object as implemented. The biometrics() method, which handles biometric authentication, needs to use biometric data registered on the device, so it's declared as an abstract method. We can implement S20 and S2 by inheriting from this object.
JAVA
/** * S20 object * * @author RWB * @since 2021.08.16 Mon 17:12:23 */ public class S20 extends SmartPhone { /** * Biometrics function */ @Override public void biometrics() { System.out.println("S20 biometrics feature"); } }
S20 needs all the functionality, so it uses every method of SmartPhone, leaving no unnecessary methods.
JAVA
/** * S2 object * * @author RWB * @since 2021.08.16 Mon 17:13:27 */ public class S2 extends SmartPhone { /** * Wireless charging function */ @Override public void wirelessCharge() { System.out.println("Unsupported on this device"); } /** * AR function */ @Override public void ar() { System.out.println("Unsupported on this device"); } /** * Abstract biometrics function */ @Override public void biometrics() { System.out.println("Unsupported on this device"); } }
S2 is a device that doesn't support wireless charging, AR, or biometric authentication. Nevertheless, because it inherits from SmartPhone, it is forced to inherit the methods for these features regardless. What's more, since biometrics() is an abstract method, it must even be implemented despite not being needed at all. This characteristic of inheritance becomes a severe drag on development convenience when the parent object grows very large. Imagine having to individually override dozens of unnecessary methods just to handle them appropriately.
Inheritance is supposed to make it convenient to implement other objects by extending shared characteristics, but the situation above is anything but convenient. This could be due to poor design of the parent object, or inheriting from an object that doesn't match the intended purpose. Whatever the reason, the fact remains that it needs to be solved.
So how can we fix this situation? By turning each of the object's methods into its own interface. Each object only needs to inherit the instances it actually needs, so it will only inherit/implement the behaviors it requires.
JAVA
/** * Smartphone object * * @author RWB * @since 2021.08.16 Mon 16:48:03 */ public class SmartPhone { /** * Call function * * @param number: [String] phone number */ public void call(String number) { System.out.println("Connecting call to " + number); } /** * Text message sending function * * @param number: [String] phone number * @param text: [String] message content */ public void message(String number, String text) { System.out.println(number + ": " + text); } }
We changed the SmartPhone object to only have the universal behaviors that apply to every smartphone.
JAVA
/** * Wireless charging interface * * @author RWB * @since 2021.08.16 Mon 18:23:33 */ public interface WirelessChargable { /** * Abstract wireless charging function */ void wirelessCharge(); } /** * AR interface * * @author RWB * @since 2021.08.16 Mon 18:24:29 */ public interface ARable { /** * Abstract AR function */ void ar(); } /** * Biometrics interface * * @author RWB * @since 2021.08.16 Mon 18:25:08 */ public interface Biometricsable { /** * Abstract biometrics function */ void biometrics(); }
The interfaces for each feature are as shown above. Notice how each feature, which used to be a method on the SmartPhone object, has now been split off into its own interface.
Through this, both S20 and S2 can be implemented by inheriting only the objects they need.
JAVA
/** * S20 object * * @author RWB * @since 2021.08.16 Mon 17:12:23 */ public class S20 extends SmartPhone implements WirelessChargable, ARable, Biometricsable { /** * Wireless charging function */ @Override public void wirelessCharge() { System.out.println("Wireless charging feature"); } /** * AR function */ @Override public void ar() { System.out.println("AR feature"); } /** * Biometrics function */ @Override public void biometrics() { System.out.println("Biometrics feature"); } }
Here's the code for the S20 object. It extends SmartPhone and implements WirelessChargable, ARable, and Biometricsable as interfaces.
JAVA
/** * S2 object * * @author RWB * @since 2021.08.16 Mon 17:13:27 */ public class S2 extends SmartPhone { /** * Text message sending function * * @param number: [String] phone number * @param text: [String] message content */ @Override public void message(String number, String text) { System.out.println("In S2"); super.message(number, text); } }
S2 doesn't have any special features implemented, so it simply extends the basic SmartPhone object.
Since interfaces support multiple inheritance, splitting required functionality into interfaces lets you inherit only the functionality you need. Furthermore, if additional features are added later through updates, you can design interfaces using the same principle so that the objects that need them can easily gain the necessary functionality.
The Interface Segregation Principle restricts objects to having only the functionality they truly need. By preventing the inheritance/implementation of unnecessary functionality as much as possible, it removes unneeded responsibilities from an object. Large-scale objects should be broken down into interfaces as needed to improve extensibility.
When inheriting from an object, judge whether that object is suitable for what you're inheriting and whether there is any unnecessary dependent functionality, and implement and inherit objects correctly accordingly.
