Introduction

Interfaces should only be used when their added value is clear. I see too many packages that declare interfaces unnecessarily, sometimes just for the sake of using interfaces. The use of interfaces when they are not necessary is called interface pollution. This is a practice I would like to see questioned and identified more in code reviews.

Code Example

Let’s look at a code example that contains questionable design choices that raise flags for interface pollution.

Listing 1:

01 package tcp
02
03 // Server defines a contract for tcp servers.
04 type Server interface {
05	Start() error
06	Stop() error
07	Wait() error
08 }
09
10 // server is our Server implementation.
11 type server struct {
12     /* impl */
13 }
14
15 // NewServer returns an interface value of type Server
16 // with an xServer implementation.
17 func NewServer(host string) Server {
18	return &server{host}
19 }
20
21 // Start allows the server to begin to accept requests.
22 func (s *server) Start() error {
23     /* impl */
24 }
25
26 // Stop shuts the server down.
27 func (s *server) Stop() error {
28     /* impl */
29 }
30
31 // Wait prevents the server from accepting new connections.
32 func (s *server) Wait() error {
33     /* impl */
34 }

Here is the interface pollution smell list for the code in Listing 1:

  1. The package declares an interface that matches the entire API of its own concrete type.
  2. The factory function returns the interface value with the unexported concrete type value inside.
  3. The interface can be removed and nothing changes for the user of the API.
  4. The interface is not decoupling the API from change.

Let’s break down the code:

On line 04 we see the declaration of the exported interface type Server. This interface declares an exact duplication of the API declared by the unexported concrete type server declared on line 11. These two lines of code check the box for items 1 in the smell list.

Then on line 17 we see the factory function NewServer. This function creates a value of the unexported concrete type server and returns it to the user inside an exported interface value of type Server. This checks the box for item 2 in the smell list.

The next code listing shows how removing the interface changes nothing for the user:

Listing 2:

// Remove the interface and change the concrete type to be exported.

11 type Server struct {
12     /* impl */
13 }

// Have the NewServer function return a pointer of the concrete type instead
// of the interface type.

17 func NewServer(host string) *Server {
18	return &Server{host}
19 }

Having the user work with the concrete type directly doesn’t change anything for the user or the API. This change has actually improved things because the extra level of indirection to call the methods through the interface value has been removed. This checks the box for item 3 in the smell list.

Finally, if we ask what can change in the code, it is never going to be a new implementation of the Server. Having an interface to decouple the server struct type from the API is not helping the API decouple itself from change. This checks the final box for item 4 in the smell list.

Conclusion

Here are some guidelines you can follow to validate and question the use of interfaces in your code:

Use an interface:

  • When users of the API need to provide an implementation detail.
  • When API’s have multiple implementations they need to maintain internally.
  • When parts of the API that can change have been identified and require decoupling.

Trusted by top technology companies

We've built our reputation as educators and bring that mentality to every project. When you partner with us, your team will learn best practices and grow along the way.

30,000+

Engineers Trained

1,000+

Companies Worldwide

12+

Years in Business