loading . . . SOLID Go Design _This post is based on the text of my GolangUK keynote delivered on the 18th of August 2016.
A recording of the talk is available on YouTube._
This post has been translated into Simplified Chinese _by_Haohao Tian _. Thanks Haohao!_
_This post has been translated to Russian by Artem Zinoviev. Thanks Artem!_
* * *
# How many Go programmers are there in the world?
How many Go programmers are there in the world? Think of a number and hold it in your head, weāll come back to it at the end of this talk.
# Code review
Who here does code review as part of their job? [the entire room raised their hand, which was encouraging]. Okay, why do you do code review? [someone shouted out āto stop bad codeā]
If code review is there to catch bad code, then how do you know if the code youāre reviewing is good, or bad?
Now itās fine to say āthat code is uglyā or āwow that source code is beautifulā, just as you might say āthis painting is beautifulā or āthis room is beautifulā but these are subjective terms, and Iām looking for objective ways to talk about the properties of good or bad code.
# Bad code
What are some of the properties of bad code that you might pick up on in code review?
* _Rigid_. Is the code rigid? Does it have a straight jacket of overbearing types and parameters, that making modification difficult?
* _Fragile_. Is the code fragile? Does the slightest change ripple through the code base causing untold havoc?
* _Immobile_. Is the code hard to refactor? Is it one keystroke away from an import loop?
* _Complex_. Is there code for the sake of having code, are things over-engineered?
* _Verbose_. Is it just exhausting to use the code? When you look at it, can you even tell what this code is trying to do?
Are these positive sounding words? Would you be pleased to see these words used in a review of your code?
Probably not.
# Good design
But this is an improvement, now we can say things like āI donāt like this because itās too hard to modifyā, or āI donāt like this because i cannot tell what the code is trying to doā, but what about leading with the positive?
Wouldnāt it be great if there were some ways to describe the properties of good design, not just bad design, and to be able to do so in objective terms?
# SOLID
In 2002 Robert Martin published his book, _Agile Software Development, Principles, Patterns, and Practices_. In it he described five principles of reusable software design, which he called the SOLID principles, after the first letters in their names.
* Single Responsibility Principle
* Open / Closed Principle
* Liskov Substitution Principle
* Interface Segregation Principle
* Dependency Inversion Principle
This book is a little dated, the languages that it talks about are the ones in use more than a decade ago. But, perhaps there are some aspects of the SOLID principles that may give us a clue about how to talk about a well designed Go programs.
So this is what I want to spend some time discussing with you this morning.
# Single Responsibility Principle
The first principle of SOLID, the S, is the single responsibility principle.
> A class should have one, and only one, reason to change.
> āRobert C Martin
Now Go obviously doesnāt have classesāinstead we have the far more powerful notion of compositionābut if you can look past the use of the word class, I think there is some value here.
Why is it important that a piece of code should have only one reason for change? Well, as distressing as the idea that your own code may change, it is far more distressing to discover that code your code depends on is changing under your feet. And when your code does have to change, it should do so in response to a direct stimuli, it shouldnāt be a victim of collateral damage.
So code that has a single responsibility therefore has the fewest reasons to change.
## Coupling & Cohesion
Two words that describe how easy or difficult it is to change a piece of software are coupling and cohesion.
Coupling is simply a word that describes two things changing togetherāa movement in one induces a movement in another.
A related, but separate, notion is the idea of cohesion, a force of mutual attraction.
In the context of software, cohesion is the property of describing pieces of code are naturally attracted to one another.
To describe the units of coupling and cohesion in a Go program, we might talk about functions and methods, as is very common when discussing SRP but I believe it starts with Goās package model.
## Package names
In Go, all code lives inside a package, and a well designed package starts with its name. A packageās name is both a description of its purpose, and a name space prefix. Some examples of good packages from the Go standard library might be:
* `net/http`, which provides http clients and servers.
* `os/exec`, which runs external commands.
* `encoding/json`, which implements encoding and decoding of JSON documents.
When you use another packageās symbols inside your own this is accomplished by the `import` declaration, which establishes a source level coupling between two packages. They now know about each other.
## Bad package names
This focus on names is not just pedantry. A poorly named package misses the opportunity to enumerate its purpose, if indeed it ever had one.
What does `package server` provide? ⦠well a server, hopefully, but which protocol?
What does `package private` provide? Things that I should not see? Should it have any public symbols?
And `package common`, just like its partner in crime, `package utils`, is often found close by these other offenders.
Catch all packages like these become a dumping ground for miscellany, and because they have many responsibilities they change frequently and without cause.
## Goās UNIX philosophy
In my view, no discussion about decoupled design would be complete without mentioning Doug McIlroyās Unix philosophy; small, sharp tools which combine to solve larger tasks, oftentimes tasks which were not envisioned by the original authors.
I think that Go packages embody the spirit of the UNIX philosophy. In effect each Go package is itself a small Go program, a single unit of change, with a single responsibility.
# Open / Closed Principle
The second principle, the O, is the open closed principle by Bertrand Meyer who in 1988 wrote:
> Software entities should be open for extension, but closed for modification.
> āBertrand Meyer, Object-Oriented Software Construction
How does this advice apply to a language written 21 years later?
package main
type A struct {
year int
}
func (a A) Greet() { fmt.Println("Hello GolangUK", a.year) }
type B struct {
A
}
func (b B) Greet() { fmt.Println("Welcome to GolangUK", b.year) }
func main() {
var a A
a.year = 2016
var b B
b.year = 2016
a.Greet() // Hello GolangUK 2016
b.Greet() // Welcome to GolangUK 2016
}
We have a type `A`, with a field `year` and a method `Greet`. We have a second type, `B` which _embeds_ an `A`, thus callers see `B`ās methods overlaid on `A`ās because `A` is embedded, as a field, within `B`, and `B` can provide its own `Greet` method, obscuring that of `A`.
But embedding isnāt just for methods, it also provides access to an embedded typeās fields. As you see, because both `A` and `B` are defined in the same package, `B` can access `A`ās private `year` field as if it were declared inside `B`.
So embedding is a powerful tool which allows Goās types to be open for extension.
package main
type Cat struct {
Name string
}
func (c Cat) Legs() int { return 4 }
func (c Cat) PrintLegs() {
fmt.Printf("I have %d legs\n", c.Legs())
}
type OctoCat struct {
Cat
}
func (o OctoCat) Legs() int { return 5 }
func main() {
var octo OctoCat
fmt.Println(octo.Legs()) // 5
octo.PrintLegs() // I have 4 legs
}
In this example we have a `Cat` type, which can count its number of legs with its `Legs` method. We embed this `Cat` type into a new type, an `OctoCat`, and declare that `Octocat`s have five legs. However, although `OctoCat` defines its own `Legs` method, which returns 5, when the `PrintLegs` method is invoked, it returns 4.
This is because `PrintLegs` is defined on the `Cat` type. It takes a `Cat` as its receiver, and so it dispatches to `Cat`ās `Legs` method. `Cat` has no knowledge of the type it has been embedded into, so its method set cannot be altered by embedding.
Thus, we can say that Goās types, while being _open for extension_ , are _closed for modification_.
In truth, methods in Go are little more than syntactic sugar around a function with a predeclared formal parameter, their receiver.
func (c Cat) PrintLegs() {
fmt.Printf("I have %d legs\n", c.Legs())
}
func PrintLegs(c Cat) {
fmt.Printf("I have %d legs\n", c.Legs())
}
The receiver is exactly what you pass into it, the first parameter of the function, and because Go does not support function overloading, `OctoCat`s are not substitutable for regular `Cat`s. Which brings me to the next principle.
# Liskov Substitution Principle
Coined by Barbara Liskov, the Liskov substitution principle states, roughly, that two types are substitutable if they exhibit behaviour such that the caller is unable to tell the difference.
In a class based language, Liskovās substitution principle is commonly interpreted as a specification for an abstract base class with various concrete subtypes. But Go does not have classes, or inheritance, so substitution cannot be implemented in terms of an abstract class hierarchy.
## Interfaces
Instead, substitution is the purview of Goās interfaces. In Go, types are not required to nominate that they implement a particular interface, instead any type implements an interface simply provided it has methods whose signature matches the interface declaration.
We say that in Go, interfaces are satisfied implicitly, rather than explicitly, and this has a profound impact on how they are used within the language.
Well designed interfaces are more likely to be small interfaces; the prevailing idiom is an interface contains only a single method. It follows logically that small interfaces lead to simple implementations, because it is hard to do otherwise. Which leads to packages comprised of simple implementations connected by _common behaviour_.
## io.Reader
type Reader interface {
// Read reads up to len(buf) bytes into buf.
Read(buf []byte) (n int, err error)
}
Which brings me to `io.Reader`, easily my favourite Go interface.
The io.Reader interface is very simple; `Read` reads data into the supplied buffer, and returns to the caller the number of bytes that were read, and any error encountered during read. It seems simple but itās very powerful.
Because `io.Reader`ās deal with anything that can be expressed as a stream of bytes, we can construct readers over just about anything; a constant string, a byte array, standard in, a network stream, a gzipād tar file, the standard out of a command being executed remotely via ssh.
And all of these implementations are substitutable for one another because they fulfil the same simple contract.
So the Liskov substitution principle, applied to Go, could be summarised by this lovely aphorism from the late Jim Weirich.
> Require no more, promise no less.
> āJim Weirich
And this is a great segue into the fourth SOLID principle.
# Interface Segregation Principle
The fourth principle is the interface segregation principle, which reads:
> Clients should not be forced to depend on methods they do not use.
> āRobert C. Martin
In Go, the application of the interface segregation principle can refer to a process of isolating the behaviour required for a function to do its job. As a concrete example, say Iāve been given a task to write a function that persists a `Document` structure to disk.
// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) error
I could define this function, letās call it `Save`, which takes an `*os.File` as the destination to write the supplied `Document`. But this has a few problems.
The signature of `Save` precludes the option to write the data to a network location. Assuming that network storage is likely to become requirement later, the signature of this function would have to change, impacting all its callers.
Because `Save` operates directly with files on disk, it is unpleasant to test. To verify its operation, the test would have to read the contents of the file after being written. Additionally the test would have to ensure that `f` was written to a temporary location and always removed afterwards.
`*os.File` also defines a lot of methods which are not relevant to `Save`, like reading directories and checking to see if a path is a symlink. It would be useful if the signature of our `Save` function could describe only the parts of `*os.File` that were relevant.
What can we do about these problems?
// Save writes the contents of doc to the suppliedĀ ReadWriterCloser.
func Save(rwc io.ReadWriteCloser, doc *Document) error
Using `io.ReadWriteCloser` we can apply the Interface Segregation Principle to redefine `Save` to take an interface that describes more general file-shaped things.
With this change, any type that implements the `io.ReadWriteCloser` interface can be substituted for the previous `*os.File`. This makes `Save` both broader in its application, and clarifies to the caller of `Save` which methods of the `*os.File` type are relevant to its operation.
As the author of `Save` I no longer have the option to call those unrelated methods on `*os.File` as it is hidden behind the `io.ReadWriteCloser` interface. But we can take the interface segregation principle a bit further.
Firstly, it is unlikely that if `Save` follows the single responsibility principle, it will read the file it just wrote to verify its contentsāthat should be responsibility of another piece of code. So we can narrow the specification for the interface we pass to `Save` to just writing and closing.
// Save writes the contents of doc to the suppliedĀ WriteCloser.
func Save(wc io.WriteCloser, doc *Document) error
Secondly, by providing `Save` with a mechanism to close its stream, which we inherited in a desire to make it look like a file shaped thing, this raises the question of under what circumstances will `wc` be closed. Possibly `Save` will call `Close` unconditionally, or perhaps `Close` will be called in the case of success.
This presents a problem for the caller of `Save` as it may want to write additional data to the stream after the document is written.
type NopCloser struct {
io.Writer
}
// Close has no effect on the underlying writer.
func (c *NopCloser) Close() error { return nil }
A crude solution would be to define a new type which embeds an `io.Writer` and overrides the `Close` method, preventing `Save` from closing the underlying stream.
But this would probably be a violation of the Liskov Substitution Principle, as `NopCloser` doesnāt actually close anything.
// Save writes the contents of doc to the suppliedĀ Writer.
func Save(w io.Writer, doc *Document) error
A better solution would be to redefine `Save` to take only an `io.Writer`, stripping it completely of the responsibility to do anything but write data to a stream.
By applying the interface segregation principle to our `Save` function, 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, we can now use `Save` to save our data to anything which implements `io.Writer`.
> A great rule of thumb for Go is **accept interfaces, return structs**.
> āJack Lindamood
Stepping back a few paces, this quote is an interesting meme that has been percolating in the Go zeitgeist over the last few years.
This tweet sized version lacks nuance, and this is not Jackās fault, but I think it represents one of the first piece of defensible Go design lore.
# Dependency Inversion Principle
The final SOLID principle is the dependency inversion principle, which states:
> High-level modules should not depend on low-level modules. Both should depend on abstractions.
> Abstractions should not depend on details. Details should depend on abstractions.
> āRobert C. Martin
But what does dependency inversion mean, in practice, for Go programmers?
If youāve applied all the principles weāve talked about up to this point then your code should already be factored into discrete packages, each with a single well defined responsibility or purpose. Your code should describe its dependencies in terms of interfaces, and those interfaces should be factored to describe only the behaviour those functions require. In other words, there shouldnāt be much left to do.
So what I think Martin is talking about here, certainly the context of Go, is the structure of your import graph.
In Go, your import graph must be acyclic. A failure to respect this acyclic requirement is grounds for a compilation failure, but more gravely represents a serious error in design.
All things being equal the import graph of a well designed Go program should be a wide, and relatively flat, rather than tall and narrow. If you have a package whose functions cannot operate without enlisting the aid of another package, that is perhaps a sign that code is not well factored along package boundaries.
The dependency inversion principle encourages you to push the responsibility for the specifics, as high as possible up the import graph, to your `main` package or top level handler, leaving the lower level code to deal with abstractionsāinterfaces.
# SOLID Go Design
To recap, when applied to Go, each of the SOLID principles are powerful statements about design, but taken together they have a central theme.
The Single Responsibility Principle encourages you to structure the functions, types, and methods into packages that exhibit natural cohesion; the types belong together, the functions serve a single purpose.
The Open / Closed Principle encourages you to compose simple types into more complex ones using embedding.
The Liskov Substitution Principle encourages you to express the dependencies between your packages in terms of interfaces, not concrete types. By defining small interfaces, we can be more confident that implementations will faithfully satisfy their contract.
The Interface Substitution Principle takes that idea further and encourages you to define functions and methods that depend only on the behaviour that they need. If your function only requires a parameter of an interface type with a single method, then it is more likely that this function has only one responsibility.
The Dependency Inversion Principle encourages you move the knowledge of the things your package depends on from compile timeāin Go we see this with a reduction in the number of `import` statements used by a particular packageāto run time.
If you were to summarise this talk it would probably be; _interfaces let you apply the SOLID principles to Go programs_.
Because interfaces let Go programmers describe what their package providesānot how it does it. This is all just another way of saying ādecouplingā, which is indeed the goal, because software that is loosely coupled is software that is easier to change.
As Sandi Metz notes:
> Design is the art of arranging code that needs to work **today** , and to be easy to change **forever**.
> āSandi Metz
Because if Go is going to be a language that companies invest in for the long term, the maintenance of Go programs, the ease of which they can change, will be a key factor in their decision.
# Coda
In closing, letās return to the question I opened this talk with; How many Go programmers are there in the world? This is my guess:
> By 2020, there will be 500,000 Go developers.
> -me
What will half a million Go programmers do with their time? Well, obviously, theyāll write a lot of Go code and, if weāre being honest, not all of it will be good, and some will be quite bad.
Please understand that I do not say this to be cruel, but, every one of you in this room with experience with development in other languagesāthe languages you came from, to Goāknows from your own experience that there is an element of truth to this prediction.
> Within C++, there is a much smaller and cleaner language struggling to get out.
> āBjarne Stroustrup, The Design and Evolution of C++
The opportunity for all Go programmers to make our language a success hinges directly on our collective ability to not make such a mess of things that people start to talk about Go the way that they joke about C++ today.
The narrative that derides other languages for being bloated, verbose, and overcomplicated, could one day well be turned upon Go, and I donāt want to see this happen, so I have a request.
Go programmers need to start talking less about frameworks, and start talking more about design. We need to stop focusing on performance at all cost, and focus instead on reuse at all cost.
What I want to see is people talking about how to use the language we have today, whatever its choices and limitations, to design solutions and to solve real problems.
What I want to hear is people talking about how to design Go programs in a way that is well engineered, decoupled, reusable, and above all responsive to change.
# ⦠one more thing
Now, itās great that so many of you are here today to hear from the great lineup of speakers, but the reality is that no matter how large this conference grows, compared to the number of people who will use Go during its lifetime, weāre just a tiny fraction.
So we need to tell the rest of the world how good software should be written. Good software, composable software, software that is amenable to change, and show them how to do it, using Go. And this starts with you.
I want you to start talking about design, maybe use some of the ideas I presented here, hopefully youāll do your own research, and apply those ideas to your projects. Then I want you to:
* Write a blog post about it.
* Teach a workshop about it what you did.
* Write a book about what you learnt.
* And come back to this conference next year and give a talk about what you achieved.
Because by doing these things we can build a culture of Go developers who care about programs that are designed to last.
Thank you.
### Related posts:
1. Internets of interest #2: John Ousterhout discusses a Philosophy of Software Design
2. Inspecting errors
3. Maybe adding generics to Go IS about syntax after all
4. Should methods be declared on T or *T
https://dave.cheney.net/2018/09/16/internets-of-interest-2-john-ousterhout-discusses-a-philosophy-of-software-design