What Is The Interface Segregation Principle?

4 min read

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()

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()

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 = false
4 override fun dial() {
5 println("Dialing tone...")
6 }
8 override fun answerCall() {
9 isPhoneRinging = false
10 print("Picking up the receiver")
11 }
13 override fun makeCall(phoneNumber: Int) {
14 println("Calling $phoneNumber...")
15 dial()
16 }
18 override fun hangUp() {
19 println("Putting the receiver down")
20 }

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 = true

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()

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()
7class Pigeon: Animal {
8 override fun move() {
9 println("Pigeon is walking")
10 }
12 override fun makeNoise() {
13 println("Coo!")
14 }
16 override fun fly() {
17 println("Pigeon is flying over the city")
18 }

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 }
6 override fun makeNoise() {
7 println("Woof")
8 }
10 override fun fly() {
11 println("")
12 }

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()
5interface Moveable {
6 fun move()
9interface Voice {
10 fun makeNoise()

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 {
3 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 }
14class Pomeranian: Moveable, Voice {
16 override fun move() {
17 println("Pomeranian is running")
18 }
19 override fun makeNoise() {
20 println("Woof!")
21 }

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.

Previous post:
React - Inline If Statement, With && Operator
Next post:
Git - Delete All Branches, Except Master