ISP: The Interface Segregation
Principle
Creating fine-grained and client-specific interfaces
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
- 👏 Please clap for this publication
- 🔔 Follow me: Medium | LinkedIn | Twitter
- 🫵 Read more on writing Efficient, Reusable, and Maintainable code