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

isp

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
    }
}
Michal Slovík
Michal Slovík
Java developer and Cloud DevOps

My job interests include devops, java development and docker / kubernetes technologies.