Interface Segregation Principle | SOLID as a Rock

Vishal Chovatiya - May 26 '20 - - Dev Community

Interface Segregation Principle in C++ is the fourth & by far the simplest design principle of a series SOLID as a Rock design principles. The SOLID design principles focus on developing software that is easy to maintainable, reusable & extendable. In this article, we will see a code violating ISP, a solution to the same code, guideline & benefits of ISP.

/!\: This article has been originally published on my blog. If you are interested in receiving my latest articles, please sign up to my newsletter.

By the way, If you haven't gone through my previous articles on design principles, then below is the quick links:

  1. SRP -- Single Responsibility Principle
  2. OCP -- Open/Closed Principle
  3. LSP -- Liskov Substitution Principle
  4. ISP -- Interface Segregation Principle
  5. DIP -- Dependency Inversion Principle

The code snippets you see throughout this series of articles are simplified not sophisticated. So you often see me not using keywords like override, final, public(while inheritance) just to make code compact & consumable(most of the time) in single standard screen size. I also prefer struct instead of class just to save line by not writing "public:" sometimes and also miss virtual destructor, constructor, copy constructor, prefix std::, deleting dynamic memory, intentionally. I also consider myself a pragmatic person who wants to convey an idea in the simplest way possible rather than the standard way or using Jargons.

Note:

  • If you stumbled here directly, then I would suggest you go through What is design pattern? first, even if it is trivial. I believe it will encourage you to explore more on this topic.
  • All of this code you encounter in this series of articles are compiled using C++20(though I have used Modern C++ features up to C++17 in most cases). So if you don't have access to the latest compiler you can use https://wandbox.org/ which has preinstalled boost library as well.

Intent

Clients should not be forced to depend on interfaces that they do not use.

  • Interface Segregation Principle is very much related to the Single Responsibility Principle. What it really means is that you should always design your abstractions in such a way that the clients that are using the exposed methods do not have to get the whole pie instead. That imposing the clients with the burden of implementing methods that they don’t actually need.

Violating the Interface Segregation Principle

struct Document;

struct IMachine {
    virtual void print(Document &doc) = 0;
    virtual void fax(Document &doc) = 0;
    virtual void scan(Document &doc) = 0;
};

struct MultiFunctionPrinter : IMachine {      // OK
    void print(Document &doc) override { }
    void fax(Document &doc) override { }
    void scan(Document &doc) override { }
};

struct Scanner : IMachine {                   // Not OK
    void print(Document &doc) override { /* Blank */ }
    void fax(Document &doc) override { /* Blank */ }
    void scan(Document &doc) override {  
        // Do scanning ...
    }
};
Enter fullscreen mode Exit fullscreen mode
  • As you can see, as far as MultiFunctionPrinter was concerned it's ok to implement print(), fax() & scan() methods enforced by IMachine interface.
  • But what if you only need a Scanner or Printer, some dev still inherits IMachine & leave unnecessary methods blank or throw NotImplemented exception, either way, you are doing it wrong.

Interface Segregation Principle Example

/* -------------------------------- Interfaces ----------------------------- */
struct IPrinter {
    virtual void print(Document &doc) = 0;
};

struct IScanner {
    virtual void scan(Document &doc) = 0;
};
/* ------------------------------------------------------------------------ */

struct Printer : IPrinter {
    void print(Document &doc) override;
};

struct Scanner : IScanner {
    void scan(Document &doc) override;
};

struct IMachine : IPrinter, IScanner { };

struct Machine : IMachine {
    IPrinter&   m_printer;
    IScanner&   m_scanner;

    Machine(IPrinter &p, IScanner &s) : printer{p}, scanner{s} { }

    void print(Document &doc) override { printer.print(doc); }
    void scan(Document &doc) override { scanner.scan(doc); }
};
Enter fullscreen mode Exit fullscreen mode
  • This gives the flexibility for the clients to combine the abstractions as they may see fit and to provide implementations without unnecessary cargo. 
  • As explained in the Single Responsibility Principle. You should avoid classes & interfaces with multiple responsibilities. Because they change often and make your software hard to maintain. You should try to split up the interface into multiple interfaces based on role.

Benefits

=> Faster Compilation

  • If you have violated ISP i.e. stuffed methods together in the interface, and when method signature changes, you need to recompile all the derived classes. This is an important aspect for some compiled languages like C++ which is well known for slow compilation. While another way around is self explainable.

=> Reusability

  • Martin also mentions that "fat interfaces" — interfaces with additional useless methods — lead to inadvertent coupling between classes. Thus, an experienced dev knows coupling is the bane of reusability.

=> Maintainability

  • The much more universal ISP benefit is that by avoiding unneeded dependencies, the system becomes
    • easier to understand;
    • lighter to test;
    • quicker to change.
  • Similarly, to the reader of your code, it would be harder to get an idea of what your class does from the class declaration line. So, if dev sees only the one god-interface that may have inherited other interfaces it will likely not be obvious. Compare
MyMachine : IMachine

to

MyMachine : IPrinter, IScanner, IFaxer
  • The latter tells you a lot, the former makes you guess at best.

Yardstick to Craft Interface Segregation Principle Friendly Software

  • This principle comes naturally when you start decomposing your problem space by identifying major roles that take part in your domain. Hence, it's never a mechanical action.
  • Following a single question to your self may help you to rectify your design:

Do I need all the methods on this interface I'm using?

Closing Notes

Even though big interfaces are a potential problem, the ISP isn't about the size of interfaces. Rather, it's about whether classes use the methods of the interfaces on which they depend. So ISP is poor guidance when designing software, but an excellent indicator of whether it’s healthy or not.

Have Any Suggestions, Query or Wants to Say Hi? Take the Pressure Off, You Are Just a Click Away.🖱️

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .