The Interface Segregation Principle (ISP) Explained in Python.

The Interface Segregation Principle (ISP) Explained in Python.
Photo by Ian Parker / Unsplash

A look at interfaces and how to write clean and maintainable code with SOLID principle 4/5.

This post is part 4 of a series on the SOLID principles.
You can find post 3 here, post 2 here and post 1 here.

In this post we look at the I- in the SOLID acronym which stands for the interface segregation principle. We start with a definition:

Clients should not be forced to depend on methods that they do not use.
Source: Agile Software Development; Robert C. Martin;

Basically, the principle is similar to the single responsibility principle in the sense that classes should only implement methods that they actually use.

Imagine we have a subclass that implements all methods in a base class. But for one particular method, the function body raises an exception since we don't want that method to be callable at all. That one particular method would go against the behavioral definition of the class and technically shouldn't be implemented. This will be a violation of the ISP principle and would require a refactor.

You might be thinking why should the sub class implement a method it doesn't need? You see, this principle deals with interfaces. When an interface is inherited all the methods present must be implemented.

Let's take a quick look at interfaces and duck typing before proceeding further.

Interfaces, abstract classes and duck typing in Python

Interfaces are a software construct that define and enforce what methods an object of a class should have. In Python we make use of duck typing to create an interface. We simply use an abstract class to represent an interface. Duck typing states that:

If it walks like a duck and it quacks like a duck, then it must be a duck
Source: Duck typing - Wikipedia

In other words if the class we define behaves like an interface we can use it as if it is an interface. Don't worry if you don't grasp this fully. The key point here is that we are treating our abstract class as an interface (by having empty method bodies and declaring methods abstract, we'll see this later).
Here's a duck for you 🦆

Enough about ducks. Time for some code 👩🏾‍💻

Code

An interface can be considered to be a fully abstract class. Therefore, we annotate all methods in the abstract class with @abstractmethod .

Once again we reuse the scenario of the previous post and consider a Car 🙂

We define an abstract base class Car . Again this can be considered to be an interface since all three methods are defined as abstract.

Next, we create two subclasses namely RegularCar and SportsCar. These two classes inherit the methods defined in Car. However notice that a RegularCar does not have turbo so we raise an exception if a call to turboAccelerate is made on a RegularCar object.

We can test the code in the __main__ function and see that a call to turboAccelerate indeed raises the exception. Everything else works as expected.

Now you may be thinking why define the turboAccelerate function for a RegularCar if it is not to be used anyway. Exactly! This is the principle of ISP. We should not force a RegularCar to implement turboAccelerate if it is not supported. In essence this is a design flaw in our model of the car.

Also, the second issue is that of code maintenance. Imagine that we need to change something in the turboAccelerate function. Suppose we need to include an extra parameter to the function definition. If we make this change to the interface we also have to make the change in RegularCar even though the function just throws an exception! In other words, we are forced to make useless changes just to make the code compile.

The fix is pretty easy. I am sure you can already see it 😎

Solution

Here is a refactor of the code:

So. Our goal was to remove turboAccelerate from a RegularCar. But we can't do that since the interface contains the method and all methods in an interface must be implemented by a subclass. Therefore, the solution is to make two interfaces. One called NoTurbo and the other called WithTurbo. We simply make turboAccelerate a function in WithTurbo and not in NoTurbo. A regular car implements NoTurbo while a turbo car implements WithTurbo.

Problem solved!

You may have noticed though that we did not exactly create two separate interfaces. Instead we extended NoTurbo with WithTurbo (inheritance). This was done to simply reduce code redundancy since there is only a single extra function in WithTurbo.

Great! As you can see now, RegularCar and SportsCar don't implement any useless functions they don't require.

Wrap Up  

In this post you learned about the interface segregation principle. Basically the principle states that a class should not be forced to implement functions that it does not use.

As with all SOLID principles, ISP contributes to making code more manageable and robust. This is particularly in the case where a change has to be made to an interface which is propagated downwards to all subclasses implementing it. Without ISP, the function being updated would have to be updated in the subclasses too even though they don't implement any business logic.

That's it for this post. Hope you learned something worthwhile!

Next week, we will reach the end and look at the last of the SOLID principles.
Stay tuned!


Enjoyed this article? Please consider subscribing using the form below😇
It is 100% free. You get an email every time I post (about once a week) + access to premium content in the future. Connect with me on LinkedIn or follow on Twitter.