Skip to main content

Command Palette

Search for a command to run...

SOLID Principles Simplified #4: Interface Segregation Principle

Why do I have to implement a method I'm never going to use?

Updated
8 min readView as Markdown

The Problem

You're building a Printer interface for an office automation system.

First requirement: Print the documents. Sounds easy. So you add print() method.

Two weeks later: "Hey, can it also scan?" Sure, you add scan().

A month later: "Can it fax too?" You sigh, but you add fax() anyway. Why not, it's just one more method.

Now your Printer interface looks complete. Print, scan, fax - a full-blown all-in-one machine.

Then someone on your team writes a class for the cheap basic printer you bought for the intern's desk. The one that only prints. Nothing else.

And now they're stuck writing this:

void fax(Document doc) override {

throw std::logic_error("This printer can't fax. Why are you even calling this?");

}

Congratulations. You just wrote a method whose entire job is to apologize for existing. If you've ever implemented an interface and thought "okay but I'm never going to use half of these methods", you've already met the problem that the Interface Segregation Principle exists to solve.

Why does this problem exist?

Nobody sits down on day one and designs a bloated interface on purpose. It happens gradually and every step along the way feels reasonable. Here is the usual stroy:

  • You start with a small and clean interface. One or two methods. Feels good.

  • A new requirement shows up. Instead of creating a new interface, you just add a method to the existing one. It's already there, everyone already implements it, why create something new?

  • This keeps happening. Every new feature quietly attaches itself to the same interface.

  • Eventually, the interface represents not one capability, but a whole bundle of unrelated capabilities.

The problem is that not every class that implements this interface actually needs all of it. So classes start doing one of two things:

  • Implementing methods they don't need with empty bodies or "not supported" exceptions.

  • Depending on methods they don't even call just because the interface forced them to bring the whole package.

Either way you've created a contract that lies. It says "Implement me and you can print, scan and fax", but half the classes implementing it can barely do one of those things.

And the real pain shows up later - when the Fax method changes and now you have to go update the basic printer class too, even though it never used fax in the first place. Your unrelated code starts breaking because of changes it has nothing to do with.

The Solution

This is exactly what the Interface Segregation Principle (ISP) - the "I" in SOLID is about.

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

In simple terms:

Don't make a class implement method it doesn't need, just because those methods happen to live in the same interface as the ones it does need.

Instead of one giant interface trying to cover every possible capability, break it into smaller, focused interfaces - so a class has to implement what's actually relevant to it.

Simple Explanation

Let's slow down and build this up from scratch.

What's an interface, really? Think of an interface as a contract or a promise. When a class implements an interface, it's promising: "I can do everything this interface says I can do."

What goes wrong with a big interface? If the interface promises 10 things, every class that implements it has to promise all 10 things too - even the classes that can honestly only do 3 of them.

What does ISP actually ask you to do? Split that one big promise into several smaller, specific promises. A class only signs up for the promises it can actually keep.

So instead of:

Printer

├── print()

├── scan()

└── fax()

You get:

Printable → print()

Scannable → scan()

Faxable → fax()

Now a class can implement Printable alone, or Printable + Scannable or all three - depending on what it can genuinely do. Nobody is forced to fake capabilities they don't have.

Real-World Analogy

Think of a universal TV remote with 200 buttons - volume, channels, Netflix, Youtube, a "3D mode" button, a "smart home" button, a button for a soundbar you don't even own.

You just wanted to change the channel. Instead, you're staring at a remote that assumes you own every device ever made.

Now compare that to a simple remote that only has the buttons relevant to your actual TV. Fewer buttons, but every single one of them does something you actually use.

That's ISP. Give classes the "remote" that matches what they actually need to do - not a universal remote loaded with buttons they'll never press.

Simple Software Example (C++)

The Bad approach - One fat Interface

class IPrinter {
  public:
    virtual void print(std::string doc) = 0;
    virtual void scan(std::string doc) = 0;
    virtual void fax(std::string doc) = 0;
    virtual ~IPrinter() = default;
};

class BasicPrinter : public IPrinter {
  public:
    void print(std::string doc) override {
        std::cout<<"Printing "<<doc<<std::endl;
    }
    
    void scan(std::string doc) override {
        throw std::logic_error("BasicPrinter cannot scan.");
    }
    
    void fax(std::string doc) override {
        throw std::logic_error("BasicPrinter cannot fax.");
    }
};

Why this is a problem:

  • BasicPrinter is forced to implement scan() and fax() even though it can't actually do either.

  • Anyone calling scan() on a BasicPrinter finds out it's broken only at runtime - the compiler happily let this through.

  • If fax()'s logic changes tomorrow, BasicPrinter has to be touched too even though it never cared about faxing.

The Better Approach - Segregated Interfaces

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

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

class IFaxable {
  public:
    virtual void fax(std::string doc) = 0;
    virtual ~IFaxable() = default;
};

class AllInOnePrinter : public IPrinter, IScanner, IFaxable {
  public:
    void print(std::string doc) override {
        std::cout<<"Printing "<<doc<<std::endl;
    }
    
    void scan(std::string doc) override {
        std::cout<<"Scanning "<<doc<<std::endl;
    }
    
    void fax(std::string doc) override {
        std::cout<<"Faxing "<<doc<<std::endl;
    }
};

class BasicPrinter : public IPrinter {
  public:
    void print(std::string doc) override {
        std::cout<<"Printing "<<doc<<std::endl;
    }
};

Why this is better:

  • BasicPrinter only implements IPrinter. No fake methods, no runtime surprises.

  • AllInOnePrinter implements all three interfaces because it genuinely supports all three capabilities.

  • Each class only depends on - and promises - exactly what it needs to.

  • Changing IFaxable now only affects classes that actually implement faxing.

Does ISP mean every interface should have just one method?

Not necessarily. ISP isn't "one method per interface" as a rule - it's "don't force unrelated capabilities into the same interface." Sometimes one method is right. Sometimes a small, cohesive group of related methods belongs together. The test is: would every class implementing this interface reasonably need all of these methods? If yes, keep them together. If no, split.

Isn't this just SRP again?

They're related but not identical. SRP is about a class having one reason to change. ISP is about an interface not forcing classes to depend on things they don't use.

Won't I end up with a huge number of tiny interfaces?

You might end up with more interfaces than before but each one is small, meaningful and easy to understand. That's a trade worth making compared to one giant interface nobody fully implements.

When should I use this?

Watch out for these signals in your own code:

  • A class implements an interface but leaves several methods empty, or throws "not supported" exceptions.

  • An interface keeps growing every time a new feature is added, regardless of whether it's related to the interface's original purpose.

  • Different implementing classes use wildly different subsets of the same interface.

Key Takeaways

If you remember only a few things from this article, remember these:

  • ISP says: don't force a class to depend on methods it doesn't use.

  • Fat interfaces lead to fake implementations - empty bodies or "not supported" exceptions.

  • The fix is to break large interfaces into smaller, focused and cohesive ones.

  • A class should only implement the interfaces that match what it can genuinely do.

  • Smaller interfaces aren't a goal by themselves - cohesion and honesty of the contract are.

From My Engineering Notebook

An interface isn't just a technical contract, it's a statement of identity. If a class implements an interface, that interface is basically saying "this class can do all of this." The moment that statement stops being true for some class, the interface has grown beyond it's purpose and it's time to split it, not stretch the class to fake-fit it.