In this article, we’ll look at a very common mistake that Golang new comers make, especially when they come from a background of Object Oriented Programming (OOP) languages like Java or C#.
What is interface pollution?
Let’s look at some codes.
package employee
type Employee struct {
Name string
Credit int
}
type EmployeeRepository interface {
Query(name string) *Employee
}
func NewEmployeeRepository() EmployeeRepository {
return &database{}
}
type database struct {}
// Query returns the first Employee that has a matching name.
func (d *database) Query(name string) *Employee {
// ...
}
We implement an employee
package, providing a method to query the storage by employee name, which is exposed through the API of the package: the EmployeeRepository
interface. This interface construct abstracts database
, the concrete implementation that does the real work. This simply follows good-old OOP design principles that we’re all used to, so what is the problem here? The first thing to note is that the EmployeeRepository
interface has only one implementation. In fact, if we remove EmployeeRepository
interface, and expose database
to the outer world like the following code, the package API does not change.
package employee
type Employee struct {
Name string
Credit int
}
type Database struct {}
func NewDatabase() *Database {
return &Database{}
}
// Query returns the first Employee that has a matching name.
func (d *Database) Query(name string) *Employee {
// ...
}
Our package users can still query employee storage by name using the Query
method of Database
. EmployeeRepository
is polluting employee
package, and returning *Database
is the better design here. In general “accept interface, return struct” is a guideline that any Go developers should keep in mind.
But what about loose coupling?
Without the EmployeeRepository
interface, our code is tightly coupled. Look at how package employee
is used in the following codes.
// how our users use the package
import employee
func GrantMonthlyCredit(d *employee.Database, name string) {
e := d.Query(name)
e.Credit += 10
}
User codes will depend on *employee.Database
, which is a concrete implementation rather than an abstration. How do we even write tests for GrantMonthlyCredit
?
Well, it’s actually true that GrantMonthlyCredit
is tightly coupled with *employee.Database
in the codes above, but it’s not because of the design of employee
package! The design of interface in Go is brilliant: instead of having to tell explicitly which type implements which interface, any types in Go can implement an interface if it implements the methods required by that interface. Interface implementation in Go is implicit (how awesome that is!). This allows us to come up with robust techniques like constructing an on-the-fly interface, and thus keeping our package design clean. It’s possible to define an interface in user codes that Database
satisfies.
import employee
// define an on-the-fly interface in user codes
type employeeRepository interface {
Query(name string) *employee.Employee
}
// use the on-the-fly interface in GrantMonthlyCredit,
// making it decoupled from the concrete implementation of *employee.Database
func GrantMonthlyCredit(r employeeRepository, name string) {
e := r.Query(name)
e.Credit += 10
}
To test GrantMonthlyCredit
, we can create a stub, or use a mocking framework to create a programmable mocked version of employeeRepository
. In our production code, we inject the real implementation: the Database
we get from employee
package.
// employeeRepositoryStub is a stub we can use in our unit tests
type employeeRepositoryStub struct{}
func (s *employeeRepositoryStub) Query(name string) *employee.Employee {
return &Employee{Name: name, Credit: 5}
}
// in unit tests
r := new(employeeRepositoryStub)
GrantMonthlyCredit(r, "john")
// in production code
d := employee.NewDatabase()
GrantMonthlyCredit(d, "john")
The exception
We learn that “accept interface, return struct” can keep our package design clean. However, it’s perfectly ok to return an interface in our package API if we have multiple implementations for it. Let’s take a look at the standard context
package.
func Background() Context { ... }
func TODO() Context { ... }
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { ... }
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { ... }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { ... }
func WithValue(parent Context, key, val any) Context { ... }
context
package provides a few functions that returns Context
, an interface. The concrete implementation of this interface depends on the use case that we have. For example, WithValue
returns a *valueCtx
while WithDeadline
returns a *timerCtx
. It’s not recommended to poke around the internals of these concrete types, and users are recommended to focus on the contract that Context
gives us instead. This encapsulation makes so much sense because the developer of the context
package can make aggressive changes, maybe rework how valueCtx
works, or maybe to add a new context type, without affecting the user’s codes.
Another exception is when we want to let users provide the implementations for the interfaces that our package exercises. For instance, the io
package defines the Reader
and Writer
interfaces, and a few utilities to work with these interfaces. One use case is to copy a stream of data to another, we can use the io.Copy
function with our input stream that implements the io.Reader
interface, and our output stream that implements the io.Writer
interface. All other utilities of io
are designed around these interfaces, making I/O operations in Go extremely powerful.
Summary
Avoid interface pollution. It’s always possible to create an on-the-fly interface out of a package type for decoupling. Only include an interface in your package API if one of these conditions is met:
- The user of the package will need to provide an implementation that satisfies the interface. A good example is the
io.Reader
andio.Writer
interfaces inio
package. - The package maintains a few implementations of the interface that it does not wish to expose their internals to the package user (e.g. the
context
package).