Interface Segregation Principle with C++ Abstract Classes

The Interface Segregation Principle (ISP)—the "I" in the SOLID design principles—dictates that clients should not be forced to rely on interfaces they do not use. In C++, which lacks a dedicated interface keyword, this principle is realized through pure abstract classes. By breaking down large, monolithic class definitions into smaller, role-specific abstract interfaces featuring pure virtual functions, C++ developers can create decoupled, maintainable, and highly reusable codebases. This article examines how C++ implements ISP using pure abstract classes, the architectural issues caused by "fat" interfaces, and how multiple inheritance facilitates targeted interface implementation.

The Role of Pure Abstract Classes as C++ Interfaces

A pure abstract class in C++ serves as an interface contract. It is defined as a class that contains at least one pure virtual function (designated by = 0) and typically contains no member variables or implementation logic, except for a virtual destructor.

class IPrintable {
public:
    virtual ~IPrintable() = default;
    virtual void print() const = 0;
};

Because C++ does not provide a separate interface construct like Java or C#, pure abstract classes represent the primary language mechanism for establishing compile-time and runtime polymorphic boundaries.

The Problem: "Fat" Interfaces

Violating the Interface Segregation Principle results in "fat" or "polluted" interfaces. A fat interface attempts to do too much by grouping unrelated actions into a single abstract class.

Consider a multi-function printer design:

// Violation of ISP
class IMachine {
public:
    virtual ~IMachine() = default;
    virtual void print() = 0;
    virtual void scan() = 0;
    virtual void fax() = 0;
};

If a client only requires a simple, dedicated printer, implementing IMachine forces that class to provide dummy implementations or throw runtime exceptions for scan() and fax(). This introduces unnecessary dependencies, increases the risk of runtime errors, and forces recompilation of dependent modules whenever any part of IMachine changes.

Applying ISP with Granular Abstract Classes

To conform to ISP, the broad contract is decomposed into lean, cohesive abstract classes based on specific client requirements.

class IPrinter {
public:
    virtual ~IPrinter() = default;
    virtual void print() = 0;
};

class IScanner {
public:
    virtual ~IScanner() = default;
    virtual void scan() = 0;
};

class IFax {
public:
    virtual ~IFax() = default;
    virtual void fax() = 0;
};

By segregating the interfaces:

  • A basic printer class implements only IPrinter.
  • A dedicated scanning station implements only IScanner.
  • An all-in-one office machine can implement IPrinter, IScanner, and IFax simultaneously.

Implementing Multiple Interfaces via Multiple Inheritance

Because C++ natively supports multiple inheritance, a concrete class can inherit from multiple pure abstract classes without the data redundancy issues traditionally associated with multiple inheritance (often referred to as the "diamond problem"). Since pure abstract classes declare behavior without internal state, inheriting from several of them is structurally safe and idiomatic.

class MultiFunctionDevice : public IPrinter, public IScanner, public IFax {
public:
    void print() override {
        // Implementation for printing
    }

    void scan() override {
        // Implementation for scanning
    }

    void fax() override {
        // Implementation for faxing
    }
};

Architectural Benefits in C++

Applying ISP via pure abstract classes provides distinct advantages within C++ projects:

  • Decoupled Client Code: Functions only accept pointers or references to the specific interface they require (e.g., void executePrintJob(IPrinter& printer)), preventing them from accessing or depending on scanning or faxing capabilities.
  • Minimized Compilation Cascades: Modifications to IScanner do not trigger recompilation of translation units that rely strictly on IPrinter.
  • Simplified Testing and Mocking: Smaller interfaces require fewer mocked methods in unit tests, reducing boilerplate and test maintenance overhead.