Interface Segregation Principle (ISP)
Software engineer Robert C. Martin was consulting for Xerox when in 1996 he defined the integration segregation principle:
Clients should not be forced to depend upon interfaces that they do not use.
This principle is related to the single responsibility principle in that even interfaces have to have a single purpose. Unfortunately, it’s easy to violate this principle as you add more features to your application.
The goal is to reduce the number of times you have to modify your code whenever a new feature or modification is needed. By splitting your application into multiple, independent components, you can reduce the number of modifications.
Violating ISP
The risk of making breaking-changes increases as your application grows over time. A new method can be added to an existing interface even though it implements a different responsibility. Therefore, it creates a polluted interface, which leads to bloated, fat interfaces that contain methods implementing several responsibilities.
Let’s look at the following example:
public interface ICalculator
{
int Add(int num1, int num2);
int Subtract(int num1, int num2);
int Multiply(int num1, int num2);
int Divide(int num1, int num2);
double Power(int number, double power);
double SquareRoot(double number);
}
public class BasicCalculator : ICalculator
{
public int Add(int num1, int num2)
{
return num1 + num2;
}
public int Subtract (int num1, int num2)
{
return num1 - num2;
}
public int Multiply (int num1, int num2)
{
return num1 * num2;
}
public int Divide (int num1, int num2)
{
return num1 / num2;
}
public double Power (int number, double power)
{
throw new NotSupportedException();
}
public double SquareRoot (double number)
{
throw new NotSupportedException();
}
}
public class AdvancedCalculator : ICalculator
{
public int Add(int num1, int num2)
{
return num1 + num2;
}
public int Subtract (int num1, int num2)
{
return num1 - num2;
}
public int Multiply (int num1, int num2)
{
return num1 * num2;
}
public int Divide (int num1, int num2)
{
return num1 / num2;
}
public double Power (int number, double power)
{
return Math.Pow(number, power);
}
public double SquareRoot (double number)
{
return Math.Sqrt(number);
}
}
public class BasicMathStudent
{
private BasicCalculator Calculator;
public BasicMathStudent()
{
this.Calculator = new BasicCalculator();
}
public double Calculate(string operation, int operand1, int operand2)
{
switch (operation.ToLower())
{
case "add":
return this.Calculator.Add(operand1, operand2);
case "subtract":
return this.Calculator.Subtract(operand1, operand2);
case "multiply":
return this.Calculator.Multiply(operand1, operand2);
case "divide":
return this.Calculator.Divide(operand1, operand2);
default:
throw new ArgumentException();
}
}
}
public class AdvancedMathStudent
{
private AdvancedCalculator Calculator;
public AdvancedMathStudent()
{
this.Calculator = new AdvancedCalculator();
}
public double Calculate(string operation, int number)
{
if (operation.ToLower() == "squareroot")
{
return this.Calculator.SquareRoot(number);
}
else
{
throw new ArgumentException();
}
}
public double Calculate(string operation, int operand1, int operand2)
{
switch (operation.ToLower())
{
case "add":
return this.Calculator.Add(operand1, operand2);
case "subtract":
return this.Calculator.Subtract(operand1, operand2);
case "multiply":
return this.Calculator.Multiply(operand1, operand2);
case "divide":
return this.Calculator.Divide(operand1, operand2);
case "power":
return this.Calculator.Power(operand1, operand2);
default:
throw new ArgumentException();
}
}
}
In this example, there are two calculations for each type of student: BasicMathStudent
and AdvancedMathStudent
. The BasicMathStudent
uses the BasicCalculator
, while the AdvancedMathStudent
uses the AdvancedCalculator
. Both types of calculators implement the ICalculator
interface.
If I run this code, it works fine. What’s the problem? Both calculators are implementing the ICalculator
interface. Those methods inside the ICalculator
are what calculators do, right?
Correct. But, take a closer look at the BasicCalculator
class. Notice that it is polluted by having to implement the Power
and SquareRoot
methods. The BasicCalculator
doesn’t need these methods, so why does it have to implement them? The BasicMathStudent
is not expected to deal with those two methods and doesn’t need a calculator with those functions.
It only has to implement two methods, and it will throw an error anyway if someone calls them. What’s the big deal?
Well, think about this over time as the application grows. What happens if the BasicCalculator
or BasicMathStudent
has subclasses? Those unnecessary methods that are polluting the interface get passed on.
Also, what happens if you want to add more functionality to the AdvancedCalculator
like Cos, Sin, and Tan? You would then have to update the ICalculator
interface, which in turn means adding more unnecessary methods to the BasicCalculator
.
Remove the Pollution: Segregate
How do you fix this issue with a bloated, fat interface? I gave you the answer earlier: ISP is related to the single responsibility principle. Let’s break down the ICalculator
into more specific roles.
Fix the interface definitions by leaving the rest of the application unchanged.
How?
Take the ICalculator
interface and segregate it into two new interfaces:
public interface IArithmetic
{
int Add(int num1, int num2);
int Subtract(int num1, int num2);
int Multiply(int num1, int num2);
int Divide(int num1, int num2);
}
public interface IExponents
{
double Power(int num, double power);
double SquareRoot(double num);
}
Then change the BasicCalculator
from:
public class BasicCalculator : ICalculator
To this:
public class BasicCalculator : IArithmetic
By doing so, you have removed the pollution from the BasicCalculator because it’s not implementing any methods it will never use.
Now, change the AdvancedCalculator
from:
public class AdvancedCalculator : ICalculator
To this:
public class AdvancedCalculator : IArithmetic, IExponents
Now, the AdvancedCalculator implements both the arithmetic and exponent features from these interfaces that it will use.
By keeping your interfaces lean and trim, you can prevent a lot of pollution from bloated interfaces.
Let’s Recap!
The interface segregation principle (ISP) states that “clients should not be forced to depend upon interfaces that they do not use.”
The intent of ISP is to prevent interface pollution known as fat interfaces.
Interface pollution is when an object implements an interface that has unnecessary behaviors.
Break fat interfaces down by segregating the responsibilities.
Using ISP increases long-term flexibility and maintainability of your application.
Now for the last letter in SOLID, “D,” for the dependency inversion principle.