Interface Segregation Principle in Go — Explained Using Dragon Ball

Learn the principle to remove unnecessary responsibilities from clients of interfaces in your code

Hernan Reyes
Better Programming

--

Cover

The Why

When working with interfaces, you may find that you have an interface that is implemented by various clients, but we're not all clients need to implement all the methods of the interface. This is bad because you’re forcing clients on to implement methods they don’t need, leaving empty methods like this:

func (c Client) MethodTheClientDontNeed() {  
panic("implement me")
}

The What

The Interface Segregation is part of the SOLID principles, and what it says is that the clients of an interface must implement only the methods that they need, or else you must split your interface into more specific ones, so the clients only implement the methods that they need.

Interface Segregation Principle

The How

To introduce you to this principle, I’ll be using Dragon Ball as a reference, so let’s imagine that we’re working on a new Dragon Ball game, so what we do is create a Warrior interface that’ll be implemented by all characters of the anime, for this example It’ll be Mr. Satan and Goku:

Warrior interface

As we see, both Mr. Satan and Goku, implement the Warriorinterface, but if you’ve watched the anime, you know that Goku can implement the three methods, but Mr. Satan doesn’t because he can’t Transform.

So in the example, he’ll be implementing a method that he doesn’t need — in this case, the method will be empty.

To avoid that, we’ll make use of the segregation principle, so we’ll create a Super Saiyaninterface that will have the Transformmethod that is only implemented by Goku, so we’ll end with something like this:

Now, Mr. Satan, just implements the methods that he needs, thanks to the new interface Super Saiyanthat we created that is only implemented by Goku.

And as you can see, at the end will have at least one interface that will be implemented by all clients, this will be the interface will use as a type to refer to our characters.

Code Time!

Now let’s take those references into Golang and see what the code will look like. But before that, let’s look at how it would be without the principle:

Define the Interface Warrior :

package main

type Warrior interface {
Kick()
Punch()
Transform()
}

type Warriors []Warrior

func executeWithoutISP(warriors Warriors) {
for _, warrior := range warriors {
warrior.Kick()
warrior.Punch()
warrior.Transform()
}
}

Add the clients that will implement the Warrior interface:

package main

type MRSatan struct{}

func NewMRSatan() *MRSatan {
return &MRSatan{}
}

func (m MRSatan) Kick() {
println("MRSatan kicks")
}

func (m MRSatan) Punch() {
println("MRSatan punches")
}

// The empty method that we want to avoid
func (m MRSatan) Transform() {
// do nothing
}
package main

type Goku struct{}

func NewGoku() *Goku {
return &Goku{}
}

func (g Goku) Kick() {
println("Goku kicks")
}

func (g Goku) Punch() {
println("Goku punches")
}

func (g Goku) Transform() {
println("Goku transforms into a Super Saiyan")
}

We execute the abilities of each client:

package main

func main() {
var warriors = Warriors{}
warriors = append(warriors, NewMRSatan())
warriors = append(warriors, NewGoku())

executeWithoutISP(warriors)
}

When we run the program, we get the following output:

https://asciinema.org/a/536786

Everything works well, MR. Satan kicks and punches and Goku kicks, punches, and transforms, but the underlying code is not as good as it could be because MR. Satan client is implementing a method he doesn’t need:

// The empty method that we want to avoid
func (m MRSatan) Transform() {
// do nothing
}

Let's solve this by applying the Interface Segregation principle:

Now instead of only having one interface, we created the SuperSaiyan one, so it can only be implemented by Goku :

package main

type Warrior interface {
Kick()
Punch()
}

type SuperSaiyan interface {
Transform()
}

type Warriors []Warrior

func executeWithISP(warriors Warriors) {
for _, warrior := range warriors {
warrior.Kick()
warrior.Punch()

// For each Warrior, we check if it is a SuperSaiyan
if superSaiyan, ok := warrior.(SuperSaiyan); ok {
superSaiyan.Transform()
}
}
}

Now, our MR. Satan client will only implement the Kick and Pucnhmethods:

package main

type MRSatan struct{}

func NewMRSatan() *MRSatan {
return &MRSatan{}
}

func (m MRSatan) Kick() {
println("MRSatan kicks")
}

func (m MRSatan) Punch() {
println("MRSatan punches")
}

And our Goku client will still implement the three methods:

package main

type Goku struct{}

func NewGoku() *Goku {
return &Goku{}
}

func (g Goku) Kick() {
println("Goku kicks")
}

func (g Goku) Punch() {
println("Goku punches")
}

func (g Goku) Transform() {
println("Goku transforms into a Super Saiyan")
}

Our main file remains the same as before:

func main() {
var warriors = Warriors{}
warriors = append(warriors, NewMRSatan())
warriors = append(warriors, NewGoku())

executeWithISP(warriors)
}

And if we run the program we still get the same response, but with a better underlying code:

https://asciinema.org/a/536787

Conclusion

ISP is a simple principle that helps you remove unnecessary responsibilities from your clients when implementing large interfaces used by various clients, but that can drive you to have countless interfaces depending on how big is the main interface you’re splitting so careful with that, in my experience I’ve just separated interfaces with at most 10 methods, ending with at most 3 interfaces which have worked well for me.

References

  1. Dive into Design Patterns
  2. Asciinema to record my terminal
  3. Excalidraw and Figma for the illustrations
  4. Repository

--

--

Hello, I’m a Backend dev currently working remotely and sharing my knowledge through articles and twitch if you speak spanish https://www.twitch.tv/hernanreyes_