ISP: The Interface Segregation
Principle

Creating fine-grained and client-specific interfaces

Abu Jobaer
The Startup
Published in
6 min readMay 15, 2020

--

Note: This publication demonstrates the Interface Segregation Principle in the following languages: TypeScript and PHP.

ISP stands for Interface Segregation Principle. It is one of the principles of Object-Oriented Design. Robert Cecil Martin¹ (widely known as Uncle Bob) introduces several principles in his famous book:

Agile Software Development, Principles, Patterns, and Practices

He describes five OOD principles in this book. They are collectively known as the SOLID Principles. These principles have received wide attention in the software industry. The Interface Segregation Principle is the fourth one of the five principles. Otherwise, “I” of SOLID.

Uncle Bob gives a definition of the Interface Segregation Principle in his book. The definition is as the following:

Clients should not be forced to depend on methods that they do not use.

What does the definition mean? This means that when the client classes are forced to depend on methods that they do NOT use, those client classes are subject to changes to those methods.

If such a thing happens, client classes need to degenerate those useless methods, but degenerating methods violate the Liskov Substitution Principle. We should avoid such couplings where possible. Let us dig into ISP.

Violating ISP

Before starting violating ISP, we would peek into the most valuable object-oriented design principle.

Program to an interface, not an implementation.

“Program to an interface” actually means “program to a supertype”. Generally, the supertype is represented via an interface or an abstract class. Keep in mind, the interface and the abstract class both represent abstractions. Some languages have these features like TypeScript, C#, PHP, JAVA, etc. So when we use an interface, we actually represent an abstraction.

An interface dictates that its sub-classes must implement the methods declared in it.

On the other hand, the “not an implementation” part advises that you should NOT design your code that depends on specific implementations.

Let us violate ISP. Imagine, we need a simple notification system that will notify users via Email or SMS. For our notification system, we would program to an interface, therefore, a supertype to represent an abstraction. Here is an interface named Notifiable for our notification system:

// TypeScript

interface Notifiable {
/**
* Sends emails
*
* @param {NotifyingData} data
* @return {void}
*/
sendEmail(data: NotifyingData): void;

/**
* Sends SMSs
*
* @param {NotifyingData} data
* @return {void}
*/
sendSms(data: NotifyingData): void;
}
// PHP

interface Notifiable
{
/**
* Sends emails
*
* @param array $data
* @return void
*/
public function sendEmail(array $data): void;

/**
* Sends SMSs
*
* @param array $data
* @return void
*/
public function sendSms(array $data): void;
}

According to our need, we have made an abstraction, the Notifiable interface, for the notification system. It has two abstract methods: sendEmail(), and sendSms(). We would use this interface throughout our app when needed. Because we would not stick to a specific concrete implementation.

Notice what we have done above can be done using an abstract class too. See the following code snippet:

// TypeScript

abstract class Notifiable {
/**
* Sends emails
*
* @param {NotifyingData} data
* @return {void}
*/
abstract sendEmail(data: NotifyingData): void;

/**
* Sends SMSs
*
* @param {NotifyingData} data
* @return {void}
*/
abstract sendSms(data: NotifyingData): void;
}
// PHP

abstract class Notifiable
{
/**
* Sends emails
*
* @param array $data
* @return void
*/
abstract public function sendEmail(array $data): void;

/**
* Sends SMSs
*
* @param array $data
* @return void
*/
abstract public function sendSms(array $data): void;
}

Now it is time to check whether the Notifiable interface or abstraction conforms to ISP. How would we know that? If we create some sub-classes from the Notifiable interface, then we would be able to see if there is any problem.

Here we will use ABC Email, for example, as the email sender service. We need to create a client class implementing the Notifiable interface. This client class will be used for sending emails. Look at the code below:

// TypeScript

class AbcEmailClient implements Notifiable {
sendEmail(data: NotifyingData): void {
console.log('Sending email...');
}

sendSms(data: NotifyingData): void {
// Degeneration of this method
}
}
// PHP

class AbcEmailClient implements Notifiable
{
public function sendEmail(array $data): void
{
var_dump('Sending email...');
}

public function sendSms(array $data): void
{
// Degeneration of this method
}
}

As the AbcEmailClient class is implementing a Notifiable interface, it has to implement sendEmail() and sendSms() methods. Notice, the sole purpose of AbcEmailClient class is to send emails, not SMSs. Nevertheless, it has to degenerate that method to be compatible with the Notifiable interface. This is an ISP violation. Because the AbcEmailClient class is forced to implement sendSms() method even though it is not needed.

What if you create a XyzSmsClient class implementing the Notifiable interface to send SMSs? In that case, the class will also have to degenerate the sendEmail() method which would be an ISP violation too.

The current design of the interface is called an interface pollution. The Notifiable interface design has low cohesion. Therefore, it has a couple of unrelated methods. The sendEmail() method sends emails while the sendSms() method sends SMSs. Both have different contexts. That is why they are not related.

As we have a low cohesive interface: the interface has unrelated methods. So we need to break the interface into groups to achieve high cohesion: an interface with deeply related methods.

Applying ISP: Separate Interfaces

With the design of the Notifiable interface, we have dealt with two separate clients — AbcEmailClient and XyzSmsClient. Both clients are forced to implement a method that both do not require. We can fix this problem by applying ISP to the Notifiable interface. Therefore, we need to break down the interface into two parts so that no client is forced to implement non-required method(s).

Interface for sending emails:

// TypeScript

interface Notifiable {
/**
* Sends emails
*
* @param {NotifyingData} data
* @return {void}
*/
sendEmail(data: NotifyingData): void;
}
// PHP

interface Notifiable
{
/**
* Sends emails
*
* @param array $data
* @return void
*/
public function sendEmail(array $data): void;
}

Interface for sending SMSs:

// TypeScript

interface Notifiable {
/**
* Sends SMSs
*
* @param {NotifyingData} data
* @return {void}
*/
sendSms(data: NotifyingData): void;
}
// PHP

interface Notifiable
{
/**
* Sends SMSs
*
* @param array $data
* @return void
*/
public function sendSms(array $data): void;
}

You may name these two interfaces after their contexts. I leave them on purpose.

We have two separate interfaces now. We can implement them with different client classes. No client class will be forced anymore to use the methods that they do not need.

// TypeScript

class AbcEmailClient implements Notifiable {
sendEmail(data: NotifyingData): void {
console.log('Sending email...');
}
}

class XyzSmsClient implements Notifiable {
sendSms(data: NotifyingData): void {
console.log('Sending SMSs...');
}
}
// PHP

class AbcEmailClient implements Notifiable
{
public function sendEmail(array $data): void
{
var_dump('Sending email...');
}
}

class XyzSmsClient implements Notifiable
{
public function sendSms(array $data): void
{
var_dump('Sending SMSs...');
}
}

Notice that if you want you can merge the sendEmail() and the sendSms() methods into one, for example, send() method or notify() method. Notice again, this may be possible in this scenario, but not in others.

// TypeScript

interface Notifiable {
/**
* Send notifications
*
* @param {NotifyingData} data
* @return {void}
*/
notify(data: NotifyingData): void;
}
// PHP

interface Notifiable
{
/**
* Sends notifications
*
* @param array $data
* @return void
*/
public function notify(array $data): void;
}

Keep in mind, separating interfaces does NOT mean that each interface will contain a single method to be implemented. An interface can have more than one method but those methods must have high cohesion. Otherwise, the interface may violate ISP.

Summary

The Interface Segregation Principle (ISP) deals with the disadvantages of fat interfaces. It encourages the creation of fine-grained and client-specific interfaces, rather than having a single interface with numerous methods.

The principle advises that clients should not be forced to depend on methods they do not use. By separating interfaces into smaller, more specialized ones, ISP promotes flexibility, reusability, and easier maintenance.

It avoids unnecessary dependencies and potential issues caused by changes in unused methods. ISP enables easier extensibility, promotes code reusability, and enhances overall system design.

See full code here: TypeScript and PHP.

Thanks for reading

--

--

Abu Jobaer
The Startup

Advocate Clean Code, SOLID Principles, Software Design Patterns & Principles, and TDD.