CLOSE

In this article, we will get an in-depth understanding of the fourth principle in the SOLID principle of object-oriented design – the Interface Segregation Principle (ISP). We will explore its definition, why it matters, how it's often violated, and how to implement it correctly in C++ using real world examples. By the end, you will see how ISP leads to cleaner, more modular,, and maintainable code.

What Is the Interface Segregation Principle (ISP)?

The Interface Segregation Principle (ISP) is one of the five SOLID principles of object-oriented design. It states:

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

In Simpler terms:

An interface should contain only those methods that are relevant to the implementing class. Large and generic interfaces should be split into smaller, more specific ones. This ensures that a class only implements what it truly needs—making your code cleaner, more modular, and easier to maintain.

A class should not be forced to implement interfaces it does not use

In C++, ISP can be implemented by breaking down larger interface into smaller, more specific interfaces. This allows clients to only depend on the specific method they need, rather than being forced to implement unnecessary methods.

Why ISP Matters

Without ISP, developers create “fat” interfaces – interfaces with multiple methods that are not always applicable to every implementation. This leads to:

  • Tight coupling
  • Unnecessary code
  • Reduced maintainability
  • Increased risk of bugs when modifying interfaces

Real-World Example: Library Management System

Imagine you are developing software for a library management system. You have different types of users: regular users who can borrow and return books, and administrators who can also add and remove books from the library's collection. Now, let's see how ISP applies in designing the interface for these users.

❌ Without ISP: One Interface for All

Initially, you might create a single interface called UserInterface, which includes methods for all user actions:

class UserInterface {
public:
    virtual void borrowBook(Book book) = 0;
    virtual void returnBook(Book book) = 0;
    virtual void addBook(Book book) = 0; // Only applicable for administrators
    virtual void removeBook(Book book) = 0; // Only applicable for administrators
};

In this scenario, both regular users and administrators are forced to implement method they don't need. Regular users have no use for addBook() and removeBook() methods, leading to unnecessary coupling and potential confusion.

✅ With ISP: Split Interfaces by Role

Applying ISP, we segregate the interfaces based on the specific need of each user type:

class UserInterface {
public:
    virtual void borrowBook(Book book) = 0;
    virtual void returnBook(Book book) = 0;
};

class AdminInterface {
public:
    virtual void addBook(Book book) = 0;
    virtual void removeBook(Book book) = 0;
};

Now, regular users only need to implement UserInterface, while administrators implement both UserInterface, and AdminInterface. This separation of concerns results in more cohesive interfaces and reduced the risk of unintended dependencies.

Implementation:

class RegularUser : public UserInterface {
public:
    void borrowBook(Book book) override {
        // Implement borrowing logic
    }

    void returnBook(Book book) override {
        // Implement returning logic
    }
};

class Administrator : public UserInterface, public AdminInterface {
public:
    void borrowBook(Book book) override {
        // Implement borrowing logic
    }

    void returnBook(Book book) override {
        // Implement returning logic
    }

    void addBook(Book book) override {
        // Implement adding book logic
    }

    void removeBook(Book book) override {
        // Implement removing book logic
    }
};

Another Violation of ISP: The Fat Worker Interface

Consider a system with a Worker interface for employees in a company:

class Worker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;
    virtual void sleep() = 0;
};

If you have a RobotWorker, it doesn't need to implement methods like eat or sleep. However, due to the fat interface, it's forced to provide dummy implementations:

class RobotWorker : public Worker {
public:
    void work() override {
        cout << "Robot is working..." << endl;
    }

    void eat() override {
        // Not applicable for robots
    }

    void sleep() override {
        // Not applicable for robots
    }
};

⚠️ Why This Violates ISP:

  1. Unnecessary Code: RobotWorker implements methods irrelevant to its behavior.
  2. Future Risks: Changes to the interface may require meaningless updates in RobotWorker.

✅ Adhering to ISP

Split the fat interface into smaller, more cohesive interfaces:

class Workable {
public:
    virtual void work() = 0;
    virtual ~Workable() = default;
};

class Feedable {
public:
    virtual void eat() = 0;
    virtual ~Feedable() = default;
};

class Restable {
public:
    virtual void sleep() = 0;
    virtual ~Restable() = default;
};

Now, HumanWorker and RobotWorker can implement only the interfaces they require:

class HumanWorker : public Workable, public Feedable, public Restable {
public:
    void work() override {
        cout << "Human is working..." << endl;
    }
    void eat() override {
        cout << "Human is eating..." << endl;
    }
    void sleep() override {
        cout << "Human is sleeping..." << endl;
    }
};

class RobotWorker : public Workable {
public:
    void work() override {
        cout << "Robot is working..." << endl;
    }
};

ISP Violated in Shape Design

#include <iostream>
#include <stdexcept>

using namespace std;

// Single interface for all shapes (Violates ISP)
class Shape {
public:
    virtual double area() = 0;
    virtual double volume() = 0; // 2D shapes don't have volume!
};

// Square is a 2D shape but is forced to implement volume()
class Square : public Shape {
private:
    double side;

public:
    Square(double s) : side(s) {}

    double area() override {
        return side * side;
    }

    double volume() override {
        throw logic_error("Volume not applicable for Square"); // Unnecessary method
    }
};

// Rectangle is also a 2D shape but is forced to implement volume()
class Rectangle : public Shape {
private:
    double length, width;

public:
    Rectangle(double l, double w) : length(l), width(w) {}

    double area() override {
        return length * width;
    }

    double volume() override {
        throw logic_error("Volume not applicable for Rectangle"); // Unnecessary method
    }
};

// Cube is a 3D shape, so it actually has a volume
class Cube : public Shape {
private:
    double side;

public:
    Cube(double s) : side(s) {}

    double area() override {
        return 6 * side * side;
    }

    double volume() override {
        return side * side * side;
    }
};

int main() {
    Shape* square = new Square(5);
    Shape* rectangle = new Rectangle(4, 6);
    Shape* cube = new Cube(3);

    cout << "Square Area: " << square->area() << endl;
    cout << "Rectangle Area: " << rectangle->area() << endl;
    cout << "Cube Area: " << cube->area() << endl;
    cout << "Cube Volume: " << cube->volume() << endl;

    try {
        cout << "Square Volume: " << square->volume() << endl; // Will throw an exception
    } catch (logic_error& e) {
        cout << "Exception: " << e.what() << endl;
    }
    
    return 0;
}

Issues:

  • Square and Rectangle are 2D shapes and shouldn't be forced to implement volume().

Sample Problematic Code:

class Square : public Shape {
    double side;
public:
    Square(double s) : side(s) {}
    double area() override { return side * side; }
    double volume() override { throw logic_error("Not applicable"); }
};

This results in runtime exceptions and code clutter.

✅ ISP-Compliant Shape Design

Split the interfaces into smaller.

#include <iostream>

using namespace std;

class TwoDimensionalShape {
public:
    virtual double area() = 0;
};

class ThreeDimensionalShape {
public:
    virtual double area() = 0;
    virtual double volume() = 0;
};

Example Classes:

class Square : public TwoDimensionalShape {
    double side;
public:
    Square(double s) : side(s) {}
    double area() override { return side * side; }
};

class Cube : public ThreeDimensionalShape {
    double side;
public:
    Cube(double s) : side(s) {}
    double area() override { return 6 * side * side; }
    double volume() override { return side * side * side; }
};

Output:

int main() {
    TwoDimensionalShape* square = new Square(5);
    ThreeDimensionalShape* cube = new Cube(3);

    cout << "Square Area: " << square->area() << endl;
    cout << "Cube Area: " << cube->area() << endl;
    cout << "Cube Volume: " << cube->volume() << endl;

    delete square;
    delete cube;

    return 0;
}

Key Takeaways

  • Avoid fat interfaces that force classes to implement irrelevant methods.
  • Split interfaces by responsibility to maintain flexibility and reduce coupling.
  • Following ISP leads to cleaner, more testable, and more reusable code.

Notes

interface-segregation-1.jpg

ISP Violated

#include <iostream>
#include <stdexcept>

using namespace std;

// Single interface for all shapes (Violates ISP)
class Shape {
public:
    virtual double area() = 0;
    virtual double volume() = 0; // 2D shapes don't have volume!
};

// Square is a 2D shape but is forced to implement volume()
class Square : public Shape {
private:
    double side;

public:
    Square(double s) : side(s) {}

    double area() override {
        return side * side;
    }

    double volume() override {
        throw logic_error("Volume not applicable for Square"); // Unnecessary method
    }
};

// Rectangle is also a 2D shape but is forced to implement volume()
class Rectangle : public Shape {
private:
    double length, width;

public:
    Rectangle(double l, double w) : length(l), width(w) {}

    double area() override {
        return length * width;
    }

    double volume() override {
        throw logic_error("Volume not applicable for Rectangle"); // Unnecessary method
    }
};

// Cube is a 3D shape, so it actually has a volume
class Cube : public Shape {
private:
    double side;

public:
    Cube(double s) : side(s) {}

    double area() override {
        return 6 * side * side;
    }

    double volume() override {
        return side * side * side;
    }
};

int main() {
    Shape* square = new Square(5);
    Shape* rectangle = new Rectangle(4, 6);
    Shape* cube = new Cube(3);

    cout << "Square Area: " << square->area() << endl;
    cout << "Rectangle Area: " << rectangle->area() << endl;
    cout << "Cube Area: " << cube->area() << endl;
    cout << "Cube Volume: " << cube->volume() << endl;

    try {
        cout << "Square Volume: " << square->volume() << endl; // Will throw an exception
    } catch (logic_error& e) {
        cout << "Exception: " << e.what() << endl;
    }
    
    return 0;
}

ISP Followed

#include <iostream>

using namespace std;

// Separate interface for 2D shapes
class TwoDimensionalShape {
public:
    virtual double area() = 0;
};

// Separate interface for 3D shapes
class ThreeDimensionalShape {
public:
    virtual double area() = 0;
    virtual double volume() = 0;
};

// Square implements only the 2D interface
class Square : public TwoDimensionalShape {
private:
    double side;

public:
    Square(double s) : side(s) {}

    double area() override {
        return side * side;
    }
};

// Rectangle implements only the 2D interface
class Rectangle : public TwoDimensionalShape {
private:
    double length, width;

public:
    Rectangle(double l, double w) : length(l), width(w) {}

    double area() override {
        return length * width;
    }
};

// Cube implements the 3D interface
class Cube : public ThreeDimensionalShape {
private:
    double side;

public:
    Cube(double s) : side(s) {}

    double area() override {
        return 6 * side * side;
    }

    double volume() override {
        return side * side * side;
    }
};

int main() {
    TwoDimensionalShape* square = new Square(5);
    TwoDimensionalShape* rectangle = new Rectangle(4, 6);
    ThreeDimensionalShape* cube = new Cube(3);

    cout << "Square Area: " << square->area() << endl;
    cout << "Rectangle Area: " << rectangle->area() << endl;
    cout << "Cube Area: " << cube->area() << endl;
    cout << "Cube Volume: " << cube->volume() << endl;
    
    return 0;
}