Code review
Why code review? If code reviews are about catching bad code, how do you know if the code you’re reviewing is good or bad?
I’m looking for some objective way to talk about the good and bad attributes of code.
Bad code
You may encounter some of the following bad code in code reviews:
- Rigid– Is the code rigid? Does it have strong typing or arguments that make it difficult to modify?
- Fragile– Is the code fragile? Would a minor change to the code cause major damage to the program?
- Immobile– Is the code hard to refactor?
- Complex– Is the code too complex, is it over-designed?
- Verbose– Is the code too verbose to use? Is it hard to tell what the code is doing when you look at it?
Are you happy to see these words when you do code reviews?
Of course not.
Good design
It would be nice if there were some way of describing the attributes of good design, not just bad design, can it be done under objective conditions?
In 2002, Robert Martin’s Agile Software Development, Principles, Patterns, and Practices book mentions five Principles of reusable Software design – “SOLID” (acronym) :
- Single Responsibility Principle – Single Responsibility Principle
- Open/Closed Principle – Open/Closed Principle
- Substitution Principle – Liskov Diction Principle
- Interface Segregation Principle – Interface Segregation Principle
- Dependency Inversion Principle – Dependency Inversion Principle
The book is a bit dated, and the language is more than a decade old. But perhaps some aspect of the SOLID principle can give us a clue as to how to talk about a well-designed Go program.
1) Single Responsibility Principle – Single function principle
A class should have one, and only one, reason to change. — Robert C Martin
Now the Go language obviously doesn’t have Classses – instead, we have the idea of more powerful combinations – but if you can see past use of class, I think there’s value there.
Why is it so important that a piece of code should have only one reason to change? Of course, finding out that your code depends on code that needs to be changed is more of a pain in the neck than your own. Also, when your code has to be modified, it should respond to a direct stimulus, not be a victim of collateral damage.
As a result, the code has the single function principle and thus has the least reason to change.
-
Cohesion – Coupling & Cohesion –
These two words describe how easy or difficult it is to modify a piece of software code.
Coupling – Coupling is the act of two things changing together – one movement causes the other to move. Cohesion – Cohesion was linked but separate, a force of mutual attraction.
In software, cohesion refers to the property that sections of code naturally attract to one another.
To describe the coupling and cohesion of the Go language, we can talk about functions and methods, which are common when discussing the single-function principle, but I believe it begins with the Package model of the Go language.
-
Pakcage named
In the Go language, all the code is in a package. Good package design starts with its naming. The package name not only describes its purpose but is also a namespace prefix. The Go standard library has some good examples:
-
net/http
– Provides HTTP clients and services -
os/exec
– Execute external commands -
encoding/json
– Implements JSON encoding and decoding
-
Use the import declaration when using other Pakcages in your own projects, which establishes a source-level coupling between the two packages.
-
Bad pakcage naming
This focus on naming is not ostentatious. Bad naming misses the opportunity to clarify its purpose.
Bad names such as server, private, common, utils are common. These packages are like a jumble of sites, because many of them change frequently for no reason.
-
UNIX philosophy of the Go language
In my opinion, any reference to decoupling design must be made to Doug McIlroy’s Unix philosophy: the combination of small, sharp tools to solve larger tasks or tasks that are often not envisioned by the original author.
I think the Package of the Go language embodies the UNIX philosophy. In fact, each package is itself a small Go language project with a single, principled unit of variation.
2) Open / Closed Principle – The open closed principle
Bertrand Meyer once wrote:
Software entities should be open for extension, but closed for modification. — Bertrand Meyer, Object-Oriented Software Construction
How does this advice apply to today’s programming languages:
Package main type A struct {
year int
} func (A A) Greet() {FMT.Println("Hello GolangUK", A. ear)} type B struct {
A
} func (B B) Greet() {FMT.Println("Welcome to GolangUK", B.ear)} func main() {
var a a
a.ear = 2016
var b b
b.ear = 2016
a.inet () // Hello GolangUK 2016 smile.inet () // Welcome to GolangUK 2016
}Copy the code
TypeA has a year field and Greet method. TypeB inserts A into the Greet method provided by B, so that B’s method overwrites A.
But embedding is not just for methods; it also provides field access to the embedded Type. As you can see, since A and B are both in the same package, B can access A’s private year field as if B had declared it.
Embedding is therefore a powerful tool that allows the Go language Type to be open to 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
}Copy the code
In the example above, typeCat has the Legs method to calculate how many Legs it has. We embed Cat into a new typeOctoCat and declare Octocats to have five legs. However, although OctoCat defines itself to have five legs, the PrintLegs method returns 4 when called.
This is because PrintLegs are defined in typeCat. It will use Cat as its receiver, so it will use Cat’s Legs method. Cat does not know about the embedded type, so its embedding method cannot be modified.
Thus, we can say that the types of the Go language is open for extension, but closed for modification.
In fact, the Go receiver’s methods are just syntactic sugar for function with pre-declared forms of arguments:
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())
}Copy the code
The receiver of the first function is the parameter you pass in, and since Go does not know about overloading, OctoCats cannot be said to replace regular Cats, which leads to the following principle:
3) Liskov Substitution Principle – Richter’s substitution principle
This principle, developed by Barbara Liskov, basically states that two types of callers are fungible if they can’t tell the difference between their actions.
In class-based programming languages, the Richter substitution principle is usually interpreted as a specification for various concrete subclasses of an abstract base class. But Go doesn’t have class or inheritance, so it can’t be replaced with a hierarchy of abstract classes.
-
Interfaces – the Interfaces
Instead, Go interfaces have the right to be replaced. In Go, types do not have to declare an interface that they want to implement. Instead, any type that wants to implement an interface simply provides methods that match the interface declaration.
As far as Go is concerned, implicit interfaces are more satisfying than explicit ones, and this has a profound impact on the way they are used.
Well-designed interfaces are more likely to be small, and the popular practice is that an interface contains only one method. Logically, the small size of the interface makes implementation easy, and vice versa. This leads to packages consisting of simple implementations of common behavioral connections.
-
io.Reader
type Reader interface { // Read reads up to len(buf) bytes into buf. Read(buf []byte) (n int, err error) }Copy the code
Interface – IO.Reader is my favorite Go language
Interfaceio. Reader is very simple.Read reads the data into the supplied buffer and returns the number of bytes the caller Read the data and any errors during the Read. It looks simple but powerful.
Because IO.Reader can handle any data that can be converted to bytes streams, we can build readers: String constants, byte arrays, standard input, network data streams, gzip tar files, and standard output for commands executed remotely over SSH on anything.
All of these implementations are interchangeable with one another because they all fulfill the same simple contract.
Thus, the application of the Richter substitution principle to the Go language can be summed up in Jim Weirich’s dictum:
Require no more, promise no less. — Jim Weirich
Which brings us to the fourth principle of “SOLID”.
4) Interface Segregation Principle – Interface Isolation Principle
Clients should not be forced to depend on methods they do not use. — Robert C. Martin
In the Go language, the application of the interface isolation principle refers to the isolated behavior of a method to accomplish its work. For example, write a method to save a document structure to disk.
// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) errorCopy the code
I can define the Save method like this, using * os.file as the Document File. But there are problems with this.
The Save method excludes the option of saving data to a network location. If network storage requirements are added later, the method will need to be modified, which means that it will affect all callers using the method.
Because Save operates directly on files on disk, it is not easy to test. To verify its operation, the test has to read the contents of the file after it has been written. In addition, the test must ensure that f is written to a temporary location and deleted later.
* Os.file also contains a number of methods that have nothing to do with Save, such as reading the path and checking if the path is a soft connection. It would be useful if the Save method only used the parts associated with * os.file.
How do we do this:
// Save writes the contents of doc to the supplied ReadWriterCloser.
func Save(rwc io.ReadWriteCloser, doc *Document) errorCopy the code
Use IO.ReadWriteCloser to apply the interface isolation principle, which redefines the Save method to use an interface to describe a more generic type.
With the change, any type that implements the IO.ReadWriteCloser interface can replace the previous * os.file. This allows Save not only to extend its reach but also to show the callers of Save which type* os.file methods are operation-specific.
As the author of Save, I no longer have the option to call irrelevant methods on * os.file, because they are hidden in the IO.ReadWriteCloser interface. We can further apply the interface isolation principle.
First, the Save method is unlikely to maintain the single-function principle, since the contents of the file it reads should be the responsibility of another piece of code. So we can narrow down the interface and just pass in writing and closing.
// Save writes the contents of doc to the supplied WriteCloser.
func Save(wc io.WriteCloser, doc *Document) errorCopy the code
Second, providing a mechanism for Save to shut down its data stream raises another question: under what circumstances the WC will shut down. Save may call Close unconditionally or call Close if it succeeds.
It causes a problem for the Save caller if it wants to write additional data after document.
type NopCloser struct { io.Writer } // Close has no effect on the underlying writer. func (c *NopCloser) Close() error { return nil }Copy the code
An original solution would define a new type, embed IO.Writer within it, and override the Close method to prevent the Save method from closing the underlying data stream.
But that might violate the Richter’s substitution rule if NopCloser doesn’t close anything.
// Save writes the contents of doc to the supplied Writer.
func Save(w io.Writer, doc *Document) errorCopy the code
A better solution is to redefine Save to pass in only IO.Writer, stripping it of all responsibility except for writing data to the data stream.
By applying the interface isolation principle to the Save method, the most specific and general requirements function is obtained. We can now use the Save method to Save data to any place where IO.Writer is implemented.
A great rule of thumb for Go is accept interfaces, return structs. — Jack Lindamood
5) Dependency Inversion Principle – Dependency inversion principle
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
What does dependency inversion mean for the Go language:
If you apply all of the above principles, and the code has been broken down into discrete packages with clear responsibilities and purposes, your code should describe its dependent interfaces and those interfaces should only describe the functional behavior they need. In other words, they won’t change much.
So, I think the application that Martin is talking about in Go is context, the structure that you import graph from.
In Go, your import graph must be acyclic. Failure to comply with this acyclic requirement can lead to compilation errors, but more seriously, it represents a series of design errors.
All things being equal, a well-designed import diagram should be broad and relatively flat, not tall and narrow. If you have a package function that cannot operate without other packages, perhaps this is a sign that the code does not consider pakcage boundaries.
The dependency inversion principle encourages you, as much as possible, to be responsible for the details within the MainPackage or the highest-level handler, like an import diagram, and let lower-level code handle the abstract interface.
“SOLID” Go language design
To review, each of the “SOLID” principles is a powerful statement when applied to Go language design, but together they have a central theme.
- The single-function principle encourages you to build functions, types, and methods into packages that exhibit natural cohesion. Types belong to each other, functions serve a single purpose.
- The open closed principle encourages you to use embedding to combine simple Types into more complex ones.
- The Richter substitution principle encourages you to express dependencies between packages using interfaces, not concrete types. By defining small interfaces, we can be more confident that we can actually meet their contracts.
- The interface isolation principle encourages you to define functions and methods only based on the desired behavior. If your function only needs the interface of a method as an argument, it probably has only one responsibility.
- Dependency inversion principleYou are encouraged to remove package dependencies at compile time – we can see in the Go language that this makes use of a particular package at runtime
import
The number of statements is reduced. (Updated)
If I were to summarize this talk, it would look something like this:
interfaces let you apply the SOLID principles to Go programs
Because interface describes the rules of their pakcage, not how. Another word is “decouple,” and that’s really the goal, because decoupled software is easier to change.
As Sandi Metz noted:
Designing 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 programming language that companies invest in over the long term, the maintenance of Go programs and easier changes will be a key factor in their decision.
At the end
Finally, to ask how many Go programmers there are in the world, my answer is:
By 2020, there will be 500,000 Go developers. – Me
What do half a million Go programmers do? Obviously, they write a lot of Go code. To be honest, not all of them are good code, and some can be terrible.
.
Go programmers should talk more about design than framework. Reuse, not performance, should be the focus at all costs.
I’d like to see people talking today about the choices and limitations of how to use programming languages, both in designing solutions and solving real problems.
What I want to hear is people talking about how to design Go programs in a way that is careful design, decoupling, reuse, and adapting to change.
. There is a little
We need to tell the world how to write great software. Show them how to write great, composable, and easily changeable software using Go.
.
Thank you!
Related blog posts:
Golang UK Conference 2016 – Dave Cheney – SOLID Go Design
Original link: SOLID Go Design
- If there is any mistranslation or incomprehension, please comment and correct
- There will be further changes to the translation after the update
- Translation:Tian Haohao
- Email address:[email protected]