What Is The Interface Segregation Principle?
This blog post will touch on the following points:
- What is an interface?
- Using an interface
- What is the Interface Segregation Principle?
- Violation of the principle
- Adhering to the principle
What Is An Interface?
Before diving into the Interface Segregation Principle, let’s look at interfaces. An interface is defined as an abstract type, and can be described as a blueprint of a class. It can contain methods and constants.
The purpose of an interface is to specify behaviour that objects must have, whilst hiding unnecessary details. This relates to one of the key concepts in Object-Oriented Programming; abstraction.
Rather than being concerned with how behaviour is implemented, interfaces are concerned with what behaviour needs to be implemented. Let’s move onto an example.
Below is an interface called Caller
, declaring five methods, without method bodies, in Kotlin:
1interface Caller {2 fun dial()3 fun makeCall(phoneNumber: Int)4 fun ring()5 fun answerCall()6 fun hangUp()7}
Note: interfaces in Kotlin are slightly different compared to Java interfaces. In Java, it isn’t possible to specify default implementation for a method, however it is in Kotlin.
In the example below, a method body has been added to ring()
on line 4:
1interface Caller {2 fun dial()3 fun makeCall(phoneNumber: Int)4 fun ring() {5 println("RING!")6 }7 fun answerCall()8 fun hangUp()9}
Using An interface
An interface can be used with other interfaces, concrete and abstract classes. Demonstrated below is the implementation of an interface in a concrete class, and extending an interface to another interface.
Implementing An Interface In A Concrete Class
A concrete class is a class that has implemented all methods from interfaces it has implemented, or abstract classes it has inherited from. For a concrete class to implement an interface, it must apply the abstract methods specified by the interface, providing a method body for each by overriding them.
1class PublicPhone: Caller {2 var isPhoneRinging: Boolean = false34 override fun dial() {5 println("Dialing tone...")6 }78 override fun answerCall() {9 isPhoneRinging = false10 print("Picking up the receiver")11 }1213 override fun makeCall(phoneNumber: Int) {14 println("Calling $phoneNumber...")15 dial()16 }1718 override fun hangUp() {19 println("Putting the receiver down")20 }21}
In the example above, ring()
has not been implemented. This is due to the default implementation for this
method being done in the Caller
interface. However, any previously defined implementation can
be overridden, as shown below:
1override fun ring() {2 isPhoneRinging = true3}
We can say PublicPhone
is a type of Caller
, because it has implemented the methods of the interface. A class
can implement multiple interfaces. We’ll go into this in more detail shortly.
Extending An Interface To Another Interface
Interfaces can also extend from other interfaces. This means an interface can inherit the declared methods and
constants from another interface, or multiple interfaces. Below is the MobilePhone
interface, which extends from
the Caller
interface. Although the methods are not declared, the MobilePhone
interface also
has Caller
’s five methods.
1interface MobilePhone: Caller {2 fun openContactsApp()3}
A couple of other key points to note about interfaces:
- They cannot be instantiated, like concrete classes can
- They cannot store state.
What is the Interface Segregation Principle?
The Interface Segregation Principle is one of the SOLID Principles, coined by Robert C. Martin. The purpose of the principles is to ensure the design of software is maintainable, easy to understand and is flexible.
The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use. The 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 -Wikipedia
This principle relates closely to the Single Responsibility Principle. Implementing the Interface Segregation Principle successfully should result in side effects of changes being minimised across a codebase. Let’s look at an example where the principle isn’t being adhered to.
Violating The Principle
1interface Animal {2 fun move()3 fun makeNoise()4 fun fly()5}67class Pigeon: Animal {8 override fun move() {9 println("Pigeon is walking")10 }1112 override fun makeNoise() {13 println("Coo!")14 }1516 override fun fly() {17 println("Pigeon is flying over the city")18 }19}
Above is an interface called Animal
. The concrete class, Pigeon
implements the Animal
interface. The interface
appears to work well with the flying animal, which moves and makes a noise. However, when trying to apply the
interface to another class where all of the methods are not relevant, we run into some difficulty.
1class Pomeranian: Animal {2 override fun move() {3 println("Pomeranian is running")4 }56 override fun makeNoise() {7 println("Woof")8 }910 override fun fly() {11 println("")12 }13}
Above is a concrete class, Pomeranian
, which implements the Animal
interface. Unfortunately, fly()
does not
apply to a dog
. However, to be able to use the interface, the class is forced to implement the method. This is a
violation of the Interface Segregation principle. Let’s look at how this issue can be fixed.
Adhering To The Principle
1interface Flyable {2 fun fly()3}45interface Moveable {6 fun move()7}89interface Voice {10 fun makeNoise()11}
Above are three separate interfaces, containing the methods which the single interface, Animal
, previously contained.
The Flyable
, Moveable
and Voice
interfaces are role interfaces.
Role interfaces are thin interfaces, containing only methods and constants which are of use to the object implementing it. They are not bloated with unneccessary methods and are relatively easy to change. In regards to the examples above, this will allow the classes to implement only the relevant methods.
1class Pigeon: Moveable, Flyable, Voice {23 override fun move() {4 println("Pigeon is walking")5 }6 override fun fly() {7 println("Pigeon is flying over the city")8 }9 override fun makeNoise() {10 println("Coo!")11 }12}1314class Pomeranian: Moveable, Voice {1516 override fun move() {17 println("Pomeranian is running")18 }19 override fun makeNoise() {20 println("Woof!")21 }22}
The Pigeon
class implements the Moveable
, Flyable
and Voice
interfaces as they’re all relevant. As
mentioned before, a class can implement multiple interfaces. The Pomeranian
class also implements
the Moveable
and Voice
interfaces. As Flyable
is not relevant, this interface is not implemented. As a
result, the Pomeranian
class is not forced to implement the irrelevant fly()
method, adhering to the principle.
Another point to note is the separate interfaces can now be used with any relevant class. For example,
a Helicopter
class can implement the Flyable
interface, and a Human
class can implement
the Moveable and Voice
interfaces.
In conclusion, interfaces can be said to be a blueprint of the methods and constants a class should implement. It is concerned with what behaviour is implemented, but not how it is done.
Separating larger interfaces into smaller role interfaces prevents classes from having to implement methods and constants which are not applicable. As a result, adhering to Interface Segregation Principle should minimise the side effects of any changes being made.
Discussion