Learn the principle to remove unnecessary responsibilities from clients of interfaces in your code
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 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.
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:
As we see, both Mr. Satan and Goku, implement the Warrior
interface, 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 Saiyan
interface that will have the Transform
method 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 Saiyan
that 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.
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 maintype 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 maintype 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 maintype 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 mainfunc main() {
var warriors = Warriors{}
warriors = append(warriors, NewMRSatan())
warriors = append(warriors, NewGoku())
executeWithoutISP(warriors)
}
When we run the program, we get the following output:
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 maintype 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 Pucnh
methods:
package maintype 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 maintype 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:
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.
- Dive into Design Patterns
- Asciinema to record my terminal
- Excalidraw and Figma for the illustrations
- Repository
Source link
Leave a Reply