Avoiding interface pollution in go
Add to bookmarksFri Jan 01 2021
Interfaces are quite easily one of Go's most used features, making it also one of the most abused features. Firstly, what is an interface? An interface acts as a contract of functionalities expected by a caller (known as a client object, or function in the case of go) be implemented by any struct (known as the service object), that will be used by the client. That way, the client object does not need to know the concrete value of the service object, nor the details and the how said functions are implemented.
So how can interfaces be abused? One way is using an interface when it is not necessary. Take a look at this example of an unnecessary interface:
package jobs
type RPCDispatcher interface {
SendMessage(message interface{}) error
Close() error
IsClosed() bool
}
type sender struct {
Closed bool
// extra fields
}
func (s *sender) SendMessage(message interface{}) error {
// send message
return nil
}
func (s *sender) Close() error {
s.Closed = true
return nil
}
func (s *sender) IsClosed() bool {
return s.Closed
}
func NewDispatcher() RPCDispatcher {
return &sender{false}
}
How is the RPCDispatcher
interface unnecessary? The package only exports a few functions and properties, one is the interface itself and the other is a constructor. Now the constructor creates a Dispatcher
which means control currently lies in the package's hand, and by control, I mean the implementation details of the interface.
Now, in that case, replacing the interface completely and simply exporting the Sender
struct would not affect the flow of logic in any way and would even boost readability.
Also if you notice, the presence of the interface in here does not provide a form of "Inversion of Control". What that means is the client here can not define the implementation detail or provide a custom implementation for this functionality, which is one main use of interfaces. Hence, the interface here is unnecessary.
So basically, you only want to use interfaces if:
- Multiple implementations of a certain functionality exist, e.g having a Dispatcher interface along with different types of dispatchers in the package RPCDispatcher, HTTPDispatcher, SQSDispatcher
- You want to offer some level of inversion of control and give clients of your package control over the implementation of some functionality e.g Having an extra
Notification
struct/package that requires aDispatcher
.
Bloated Interfaces
Another Way interfaces can be misused or polluted is by being unnecessarily fat (yes, we're fat-shaming interfaces now). What does that mean? It means the interfaces are bloated and not role-specific, thereby forcing multiple clients (and service objects) to implement methods they do not need. And according to the Interface Segregation Principle of Solid:
Clients should not be forced to depend upon interfaces that they do not use.
Take this code for example:
package transport
import "errors"
type Vehicle interface {
Start() error
ChangeGear() error
Move() error
Refuel() error
}
type car struct {
//...
}
func (c *car) Start() error {
return nil
}
func (c *car) ChangeGear() error {
return nil
}
func (c *car) Move() error {
return nil
}
func (c *car) Refuel() error {
return nil
}
type bicycle struct {
//...
}
func (b *bicycle) Start() error {
return errors.New("unimplemented")
}
func (b *bicycle) ChangeGear() error {
return errors.New("unimplemented")
}
func (b *bicycle) Move() error {
return nil
}
func (b *bicycle) Refuel() error {
return errors.New("unimplemented")
}
Now we can see that we have an overly bloated Vehicle interface, the current implementations of the vehicle interface have to implement the extra methods which are Start, Refuel e.t.c. This also means that whatever client is using this interface would need to depend on the Refuel (and other) method even if it simply needs a bicycle-type interface.
A neater way to do this would be to split the Vehicle interface into role-based interfaces like so:
type MotorVehicle interface {
Vehicle
GearedVehicle
Start() error
Refuel() error
}
type GearedVehicle interface {
ChangeGear() error
}
type Vehicle interface {
Move() error
}
//...
That way clients and services alike aren't forced to depend on and implement functionality that they do not need.
Conclusion
Only use interfaces when you need to, and avoid fat interfaces otherwise your entire codebase could get bloated very fast.