Avoiding interface pollution in go

Add to bookmarks

Fri 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 a Dispatcher.

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.