SOLID and Interface Segregation Principle
Overview
In this series of articles, we will cover all the SOLID patterns and their use in examples.
The SOLID principles were introduced by Robert C. Martin in his 2000 paper “Design Principles and Design Patterns.”
The SOLID principles consist of the following five concepts:
Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
Interface Segregation Principle
Clients should not be forced to depend upon interfaces that they do not use.
Why
In previous article Liskov Substitution principle we discuss using inheritance and relationship between components. And now we can look at the last omitted part of the Interface Segregation Principle.
The definition of the interface segregation principle states that clients should not be forced to depend on methods that they do not use. In simpler form keep interfaces short and focused. Code can benefit a lot from using the interface segregation principle properly. Lean interfaces minimize dependencies on unused members and reduce code coupling. We will also reinforce the use of the single responsibility principle and the Liskov substitution principle along the way. This principle help one another.
When and How to implement this principle
There are multiple cases, when should be used this principle. For better understanding we go through multiple examples with violates this principle, and we can try to design solution.
Client is breaking Single Responsibility
public interface Printer {
boolean print();
boolean scan();
boolean fax();
}
public class MultifunctionalFaxPrinter implements Printer {
@Override
public boolean print() {
//Logic for printing
return true;
}
@Override
public boolean scan() {
//Logic for scanning
return true;
}
@Override
public boolean fax() {
//Logic for faxing
return true;
}
public boolean phone() {
return true;
}
}
One possible solution is to create multiple separate interfaces for each part of the printer.
public interface Scan {
boolean scan();
}
public interface Printer {
boolean print();
}
public interface Fax {
boolean fax();
}
public class MultifunctionalFaxPrinter implements Printer, Fax, Scan {
@Override
public boolean print() {
//Logic for printing
return true;
}
@Override
public boolean scan() {
//Logic for scanning
return true;
}
@Override
public boolean fax() {
//Logic for faxing
return true;
}
public boolean phone() {
return true;
}
}
And if we only want to use a simple printer and a simple scanner, we do not need to implement extensive interfaces.
public class BasicPrinter implements Printer {
@Override
public boolean print() {
//Logic for printing
return true;
}
}
public class BasicScanner implements Scan {
@Override
public boolean scan() {
//Logic for scanning
return true;
}
}
There can be small improvement with naming for interfaces. When using the names Printer
or Scan
, it can be confusing for developers to associate them with an entity.
Better interface names should end in -able
: e.g. Printable
, Scannable
and Faxable
.
The client has implemented methods but does not fill them
Here we have two examples of violation this principle. In both cases client code extends Account
abstract class.
However, the client does not fill in the withdraw
method. In the first case the client leaves the method empty and in the second case throws an exception.
public abstract class Account {
protected abstract void deposit(BigDecimal amount);
protected abstract void withdraw(BigDecimal amount);
}
/**
* Empty method
*/
public class FixedTermDepositAccount extends Account {
protected void deposit(BigDecimal amount) {
// Deposit into this account
}
protected void withdraw(BigDecimal amount) {
// Empty method
}
}
/**
* Or throw exception
*/
public class FixedTermDepositAccount extends Account {
@Override
protected void deposit(BigDecimal amount) {
// Deposit into this account
}
@Override
protected void withdraw(BigDecimal amount) {
throw new UnsupportedOperationException("Withdrawals are not supported by FixedTermDepositAccount!!");
}
}
We can fix that by multiple cases. Firstly we keep Account
class, but we separated logic for deposit and withdraw into interfaces.
public interface Depositable {
void deposit(BigDecimal amount);
}
public interface Withdrawable {
void withdraw(BigDecimal amount);
}
public abstract class Account {
// keep for private
}
public class FixedTermDepositAccount extends Account implements Deposable {
@Override
protected void deposit(BigDecimal amount) {
// Deposit into this account
}
}
So FixedTermDepositAccount
is only special case of Account without withdraw
method and implementation.
And in case we want to use classic Account with all services:
public class CurrentAccount extends Account implements Deposable, Withdrawable {
@Override
protected void deposit(BigDecimal amount) {
// Deposit into CurrentAccount
}
@Override
protected void withdraw(BigDecimal amount) {
// Withdraw from CurrentAccount
}
}