Not lacking nuance Link to heading
I’ve mentioned a general guideline of accept interfaces, return structs in a previous post and multiple times on code reviews to coworkers, but often get the question “Why”. Especially since this isn’t a hard rule. The crux of the idea, and understanding when to bend it, is in the balance of avoiding preemptive abstractions while maintaining flexibility.
Preemptive abstractions make systems complex Link to heading
All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections
- David J. Wheeler
Software engineers love abstractions. Personally, I’ve never seen a coworker more involved in writing code than when they are creating an abstraction for something. Interfaces abstract away from structures in Go and this indirection has a non zero level of embedded complexity. Following the You aren’t gonna need it philosophy of software design, it doesn’t make sense to create this complexity until it’s needed. A common reason to return interfaces from function calls is to let users focus on the API emitted by a function. This isn’t needed with Go because of implicit interfaces. The public functions of the returned struct become that API.
Always [abstract] things when you actually need them, never when you just foresee that you need them.
Some languages require you to foresee every interface you’ll ever need. A great advantage of implicit interfaces is that they allow graceful abstraction after the fact without requiring you to abstract up front.
Need is in the eye of the beholder Link to heading
when you actually need them
How do you define when an abstraction is needed? For the return type, this is easy. You’re the one writing the function so you know exactly when you need to abstract the return value.
For function inputs, the need isn’t in your control. You may think your database struct is enough, but a user could have a need to wrap it with something else. It’s difficult, if not impossible, to anticipate the state of everyone using your function. This imbalance between being able to precisely control the output, but be unable to anticipate the user’s input, creates a stronger bias for abstraction on the input than it does on the output.
Remove dead code details Link to heading
Another aspect of simplification is removing unnecessary details. Functions are like cooking recipes: given this input and you get a cake! No recipe lists ingredients it doesn’t need. Similar, functions shouldn’t list inputs they don’t need. What would you think of the following function?
func addNumbers(a int, b int, s string) int {
return a + b
}
It’s obvious to most programmers that parameter s does not belong. What’s less obvious is when the parameters are structures.
type Database struct{ }
func (d *Database) AddUser(s string) {...}
func (d *Database) RemoveUser(s string) {...}``func NewUser(d *Database, firstName string, lastName string) {
d.AddUser(firstName + lastName)
}
Just like a recipe with too many ingredients, NewUser takes a Database object that can do too many things. It only needs AddUser, but takes something that also has RemoveUser. Interfaces allow us to create the function that only depends upon what we need.
type DatabaseWriter interface {
AddUser(string)
}
func NewUser(d DatabaseWriter, firstName string, lastName string) {
d.AddUser(firstName + lastName)
}
Dave Cheney wrote about this point when he described Interface Segregation Principle. He also describes other advantages of limiting input that’s worth reading. The summary goal that drives home the idea is:
the results has simultaneously been a function which is the most specific in terms of its requirements–it only needs a thing that is writable–and the most general in its function
I would just add that in the same way function addNumbers above obviously shouldn’t have parameter s string, function NewUser would ideally not require a database that can also remove users.
Summarize the reasons and examine exceptions Link to heading
The primary reasons listed are:
- Remove unneeded abstractions
- Ambiguity of a user’s need on function input
- Simplify function inputs
These reasons also allow us to define exceptions to the rule. For example, if your function actually can return multiple types then obviously return an interface. Similarly, if the function is private then there is no ambiguity on function input since you control that, so bias towards not preemptive abstraction. For the third rule, go has no way to abstract out struct members values. So, if your function needs to access struct members (and not just functions on the struct), then you’re forced to accept the struct directly.