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:
- There is a base behaviour that repeats in all or almost all cases.
- 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...") }
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...") }
Now, ISP is respected. In short, we have:
- All interfaces defined
- A struct called
BasicPrinter
- Implementation of
Printable
inBasicPrinter
by adding thePrint()
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
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
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
- Elixir examples: solid_elixir_examples
- Go examples: solid-go-examples
I hope the concepts are clear. If you still have questions, feel free to leave a comment.
Top comments (0)