SOLID: Interface Segregation Principle
Photo by Blue Bird from Pexels

SOLID: Interface Segregation Principle

All programs are composed of functions and data structures. The SOLID principles introduced by Robert C. Martin, help us group our code into modules and advise how to interconnect them. The whole point of application architecture is to manage change. Software requirements change all the time and we as developers need to make sure that these changes don't break existing code.

There are 5 principles that make up the acronym are:

In this post, I will attempt to explain the ISP (Interface Segregation Principle).

Within OOP, interfaces provide layers of abstraction that allows the program to depend on the interface rather than the implementation.

The ISP (Interface Segregation Principle) states:

Clients should not be forced to depend upon interfaces that they do not use

As your application grows you are tempted to add methods to an existing interface. If the methods are not related to the interface, it is better to separate that new method out into its own interface

Just as the SRP advises to streamline your classes for a single purpose, so too does the ISP guide you to split your interfaces into parts of relevance to reduce the effect of change on the application.

Just like the Liskov Substitution Principle, good indications of violating ISP are:

  • Our methods return inappropriate data
  • Our methods have to throw "not implemented" exceptions

This will make it difficult to write effective code against these interfaces.

To fix this we need to segregate the interface into sub-interfaces so that none of the classes will be forced to write inappropriate implementation methods

Let's explain using some code:

1interface Vehicle { 2 start(): void 3 stop(): void 4 drive(): void 5 fly(): void, 6 takeOff(): void 7} 8 9//implementation 10class Car implements Vehicle { 11 fly(): void { 12 throw new Error("Method not implemented.") 13 } 14 takeOff(): void { 15 throw new Error("Method not implemented.") 16 } 17 stop(): void { 18 console.log("Car Engine Stopped") 19 } 20 21 drive(): void { 22 console.log("Car Moving...") 23 } 24 start(): void { 25 console.log("Car Engine Running...") 26 } 27 28} 29 30//implementation 31class Helicopter implements Vehicle { 32 drive(): void { 33 throw new Error("Method not implemented.") 34 } 35 takeOff(): void { 36 throw new Error("Method not implemented.") 37 } 38 stop(): void { 39 console.log("Helicopter Engine Stopped") 40 } 41 42 fly(): void { 43 console.log("Helicopter Flying...") 44 } 45 46 start(): void { 47 console.log("Helicopter Engine Running...") 48 } 49 50} 51 52class TransportApp { 53 private airTransport 54 private landTransport 55 constructor(airTransport: Vehicle, landTransport: Vehicle) { 56 this.airTransport = airTransport 57 this.landTransport = landTransport 58 } 59 60 move() { 61 this.airTransport.fly() 62 this.landTransport.drive() 63 } 64} 65 66async function main() { 67 const landTransport = new Car() 68 const airTransport = new Helicopter() 69 70 const transportApp = new TransportApp(airTransport, landTransport) 71 transportApp.move() 72 73} 74 75main() 76
1OUTPUT: 2Helicopter Flying... 3Car Moving...

Note the methods in classes that are not implemented and hence the ISP is violated.

To fix this problem we can break out the common methods into a base interface and group related methods into sub-interfaces of the base.

1 2//base interface 3interface Vehicle { 4 start(): void 5 stop(): void 6 7} 8 9//sub interfaces 10interface Drivable extends Vehicle { 11 drive(): void 12} 13 14//sub interfaces 15interface Flyable extends Vehicle { 16 fly(): void, 17 takeOff(): void 18} 19 20//implementation 21class Car implements Drivable { 22 stop(): void { 23 console.log("Car Engine Stopped") 24 } 25 26 drive(): void { 27 console.log("Car Moving...") 28 } 29 start(): void { 30 console.log("Car Engine Running...") 31 } 32 33} 34 35//implementation 36class Helicopter implements Flyable { 37 takeOff(): void { 38 console.log("Helicopter taking off...") 39 } 40 stop(): void { 41 console.log("Helicopter Engine Stopped") 42 } 43 44 fly(): void { 45 console.log("Helicopter Flying...") 46 } 47 48 start(): void { 49 console.log("Helicopter Engine Running...") 50 } 51 52} 53 54class TransportApp { 55 private airTransport 56 private landTransport 57 constructor(airTransport: Flyable, landTransport: Drivable) { 58 this.airTransport = airTransport 59 this.landTransport = landTransport 60 } 61 62 move() { 63 this.airTransport.fly() 64 this.landTransport.drive() 65 } 66} 67 68async function main() { 69 const landTransport = new Car() 70 const airTransport = new Helicopter() 71 72 const transportApp = new TransportApp(airTransport, landTransport) 73 transportApp.move() 74 75} 76 77main()
1OUTPUT: 2Helicopter Flying... 3Car Moving...

We still have the same output, but our code is more robust. We will not be able to call a non-implemented method on any of the implementations

In Conclusion

It is harmful to depend on classes that have been forced to implement interfaces that they do not fully use. Interfaces should be created with a common purpose just like Single Responsibility Principle

Violating this principle will make the code fragile. Smaller interfaces are easier to implement.