4. Interface Segregation Principle (ISP)

What is Interface Segregation Principle (ISP)?

It states that a client should not be forced to depend on interface that it does not use. In other words, a class should not have to implement method that it does not need.

It states that: 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.

Explanation with example

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:

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:

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 AdminInterfce. 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
    }
};

Violation of ISP

Example: Fat 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;
    }
};