🖋️
This article is one part of my series on Robert (Bob) C. Martin's SOLID principles.

Introduction

The Interface Segregation Principle is one that Bathrobe Bob designed himself. On the surface it looks like it only applies to static languages, which it shares somewhat with the previous entry in SOLID: The Liskov Substitution Principle. Let's see how we can apply it to duck-typed languages.

Definition

From Bob's own paper, "The Interface Segregation Principle" (1996):

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

I take interfaces to mean explicit or implicit sets of public-facing methods.

A detour into statically-typed languages

The Interface Segregation Principle is all about interfaces, so it's helpful to know how they work in static languages. If you already do, you can skip this section.

We discussed typing as part of the Liskov Substitution Principle. The important takeaway was that we don't have any sort of strict "type" or "interface" in duck-typed languages like Ruby, instead requiring the programmer to conform to implicit — not explicit — contracts about what objects do.

Interfaces, as found in languages such as C#, are programming constructs which represent explicit contracts.

public interface IBird
{
    void Fly();
}

Interface for a bird in C#

The above interface is for a bird. That is to say that all classes implementing the IBird interface must respond to the method Fly(). Here is how a Sparrow might implement this interface:

public class Sparrow : IBird
{
    public void Fly()
    {
        Console.WriteLine("The sparrow flits quickly through the air.");
    }
}

A Sparrow class in C#, implementing IBird.

But there are birds that don't fly, like an Emu. An Emu should implement IBird because it is a bird, but given that it can't fly, perhaps our interface doesn't make sense.

public class Emu : IBird
{
}

An Emu class in C#. Invalid because it doesn't implement Fly().

This fails at compile time, because any classes that implement an interface must implement all of its methods.

'Emu' does not implement interface member 'IBird.Fly()'

Obviously we have misrepresented birds by writing an interface in which they are all expected to fly.

In practice, this is useful because we usually want to be able to substitute one class for another — see the Liskov Substitution Principle — provided they implement a specific interface. In the case of a bird, as incorrectly defined above, we know that it can always fly.

public class BirdLauncher
{
    public void Launch(IBird bird)
    {
        Console.WriteLine("Launching a bird!");
        bird.Fly();
    }
}

Remember: Ruby doesn't have explicit interfaces like C# does, but duck-typing itself is conceptualized around objects with implicit contract parity being interchangeable.

Now that we understand explicit interfaces in static languages, let's turn back to Ruby and how the interface substitution principle applies to it.

Violating the principle

Let's think about coffee machines. My coffee needs are simple and direct: it should taste good enough and be easy to make when I'm not awake. So I use an all-in-one unit, which takes beans and water and produces coffee.

class CoffeeMachine
  def add_water = puts "Water added."
  def add_beans = puts "Beans added."
  def make_coffee = puts "Enjoy!"
end

Given a coffee machine, a sleepy human can now make coffee.

class SleepyHuman
  def make_coffee(coffee_maker)
    coffee_maker.add_water
    coffee_maker.add_beans
    coffee_maker.make_coffee
  end
end

But there are plenty of other ways to make coffee. A capsule machine, for example, takes capsules instead of beans. Let's extend the CoffeeMachine to account for this.

class CapsuleMachine < CoffeeMachine
  def add_capsule = puts "Capsule added."
end

SleepyHuman should now also work with a capsule machine. One way to do this is check what the object responds to.

class SleepyHuman
  def make_coffee(coffee_maker)
    coffee_maker.add_water
    if coffee_maker.respond_to?(:add_capsule)
      coffee_maker.add_capsule
    else
      coffee_maker.add_beans
    end
    coffee_maker.make_coffee
  end
end

But CapsuleMachine violates the Interface Segregation Principle, because it includes methods it doesn't use: by extending CoffeeMaker, it has an add_beans method, even though it cannot accept beans.

Conforming to the principle

A better way would be to share only methods that we know all coffee makers need. We could do this by inheriting from a more generic base class, but we'll change it up a little by including methods via a module instead. Either approach works at this stage.

module CoffeeMaker
  def add_water = puts "Water added."
  def make_coffee = puts "Enjoy!"
end

class AllInOneCoffeeMachine
  include CoffeeMaker
  
  def add_beans = puts "Beans added."
end

class CapsuleMachine
  include CoffeeMaker

  def add_capsule = puts "Capsule added."
end

Conformance to ISP by not including unused methods.

This conforms to ISP because neither class includes methods that aren't used by that class.

Inevitably, we will add more coffee makers in the future. One such machine might be hooked up to a water supply and not need water adding to it.

We can improve our solution by embracing smaller interfaces, which is what the ISP guides us towards. These may also be called role interfaces.

At this stage it becomes more important that we're using modules, or mixins, rather than inheritance, as we can only inherit from one class in Ruby and most other languages.

module AcceptsBeans
  def add_beans = puts "Beans added."
end

module AcceptsCapsules
  def add_capsule = puts "Capsule added."
end

module AcceptsWater
  def add_water = puts "Water added."
end

module MakesCoffee
  def make_coffee = puts "Enjoy!"
end


class AllInOneCoffeeMachine
  include AcceptsWater
  include AcceptsBeans
  include MakesCoffee
end

class CapsuleMachine
  include AcceptsWater
  include AcceptsCapsules
  include MakesCoffee
end

class PlumbedInCoffeeMachine
  include AcceptsBeans
  include MakesCoffee
end

Using interfaces as roles to conform to ISP across more classes.

Then, the SleepyHuman need only call what the objects respond to:

class SleepyHuman
  def make_coffee(coffee_maker)
    coffee_maker.add_water if coffee_maker.respond_to?(:add_water)
    coffee_maker.add_capsule if coffee_maker.respond_to?(:add_capsule)
    coffee_maker.add_beans if coffee_maker.respond_to?(:add_beans)

    coffee_maker.make_coffee
  end
end

respond_to? checks are used to call only the methods that are implemented.

Though the coffee makers themselves pass ISP, they do violate other SOLID principles like OCP. There are several patterns that can help resolve this. Since we're talking about ISP, I'll include just one option: inverting the preparation back to the coffee makers themselves.

module MakesCoffee
  def make_coffee = puts "Enjoy!"
end

class AllInOneCoffeeMachine
  include MakesCoffee
  include AcceptsWater
  include AcceptsBeans
  
  def prepare
    add_water
    add_beans
    make_coffee
  end
end

class CapsuleMachine
  include MakesCoffee
  include AcceptsWater
  include AcceptsCapsules
  
  def prepare
    add_water
    add_capsule
    make_coffee
  end
end

class PlumbedInCoffeeMachine
  include MakesCoffee
  include AcceptsBeans
  
  def prepare
    add_beans
    make_coffee
  end
end

class SleepyHuman
  def make_coffee(coffee_maker)
    coffee_maker.prepare
  end
end

Delegating preparation to the coffee makers themselves means the human need know nothing about the coffee maker other than that it has a prepare method.

Summary

The Interface Segregation Principle is concerned with keeping interfaces lightweight by not including methods they don't use, which leads to reduced coupling.

The principle is more commonly noticed and clearly violated in statically-typed languages, where a compiler loudly complains about missing methods. It remains a valid principle in duck-typed languages when viewing interfaces as implicit contracts.

As with most of these principles, my opinion is that we need not adhere strictly to ISP immediately, so long as we bear in mind the issues violation brings as we iterate. That is: if all we have is a single type of CoffeeMaker, tight coupling between it and a SleepyHuman might be acceptable. When this changes, it's time to re-evaluate.