DEV Community

Cover image for Understanding SOLID once and for all | Part 04 - (ISP)
Rafael Honório
Rafael Honório

Posted on

Understanding SOLID once and for all | Part 04 - (ISP)

Hey folks, continuing the series about SOLID, today we will talk about the Interface Segregation Principle (ISP). In my view, this is the easiest concept of the five principles. The name already says a lot about what we will discuss, and I will also share some cool insights about interfaces.

Summary

The idea of an interface appeared at the end of the eighties, after languages like Simula and Smalltalk had already introduced most of the basics we know today as object-oriented programming.

Bertrand Meyer formally presented the interface concept in his book Object Oriented Software Construction, published in 1988. In that book, he defined the concept of contracts between software components in the Eiffel language. Since then, these techniques have been adopted in many other languages, such as Java, C#, and Ruby.

The goal of creating a contract (interface) is to show that a certain module has common behaviour that can be reused in different contexts. Classic examples are:

  • Interaction with a database
  • Interaction with cache
  • Interaction with a message broker
  • Interaction with subtypes (inheritance)

All those situations share two ideas:

  1. There is a base behaviour that repeats in all or almost all cases.
  2. Each implementation will be used in many places in your code.

The Interface Segregation Principle says:

No client should be forced to depend on methods it does not use.

So an interface should define behaviour for a specific area of your software, making the code more cohesive and less coupled.

Examples

type Printer interface {
  Print()
  PrintColorFull()
  Fax()
  Scan()
}

type BasicPrinter struct{}
func (BasicPrinter) Print() { fmt.Println("Printing...") }
Enter fullscreen mode Exit fullscreen mode

In this case, the other methods are missing, so it breaks ISP.

A better approach is to create specific interfaces:

type Printable interface { Print() }
type PrintableColor interface { PrintColorFull() }
type Scannable interface { Scan() }
type Faxable interface { Fax() }

type BasicPrinter struct{}
func (BasicPrinter) Print() { fmt.Println("Printing...") }
Enter fullscreen mode Exit fullscreen mode

Now, ISP is respected. In short, we have:

  • All interfaces defined
  • A struct called BasicPrinter
  • Implementation of Printable in BasicPrinter by adding the Print() method

Note: In Go, these links are implicit.

Below is a similar example in Elixir. The language allows generic return types, so you can implement the behaviour in other modules without returning the same data type.

In Elixir an “interface” is called a behavior or callback.

defmodule MultiFunctionDevice do
  @callback print() :: any()
  @callback fax() :: any()
  @callback scan() :: any()
end

defmodule SimplePrinter do
  @behaviour MultiFunctionDevice
  def print, do: IO.puts("Printing")
  def fax, do: :not_implemented
  def scan, do: :not_implemented
end
Enter fullscreen mode Exit fullscreen mode

The same idea applies here. Even though the function exists, returning :not_implemented or leaving empty functions shows that the interface is poorly designed and still breaks the principle.

defmodule PrinterOnly do
  @callback print() :: any()
end
Enter fullscreen mode Exit fullscreen mode

Same strategy: split the interface into smaller ones.

Conclusion

Interfaces have no mystery. ISP pushes us to split big interfaces into smaller cohesive contexts, which helps in many cases like:

  • Inheritance between objects
  • Interaction with external services (cache, message broker, database)
  • Structured error handling

What we get:

  • Less coupling
  • More cohesive code
  • Code that adapts better to change

Reference code

I hope the concepts are clear. If you still have questions, feel free to leave a comment.

Top comments (0)