译 | SOLID Go Design

Source: cyningsun.github.io//08-03-2019...

Code review

All of you who put code review as part of their daily work? [Whole room raised their hands, encouraging]. Well, why should the code review? [Someone shouted "stop malicious code"]

If the code review is to catch bad code, then you know how you are reviewing the code is good or bad?

As you might say, "It's a beautiful painting," or "The room is very nice," now you can say "Code ugly" or "source code is very beautiful," but these are subjective. I'm looking for in an objective way to talk about good or bad code feature.

Bad code

You may encounter the following characteristics of these bad code in the code review:

  • The Rigid - Code rigid yet? Or whether it has a strongly typed parameters that are difficult to modify?
  • Fragile - Code fragile it? Whether subtle changes can cause incalculable damage in the code base?
  • Immobile - the code difficult to reconstruct it? Code can be avoided simply knock on the keyboard circulation import?
  • Complex - there is no code to virtuoso, whether over-designed?
  • Verbose - consuming code uses it? When reading, to see what the code is doing it?

These words are positive it? Are you happy to see these words used to audit your code?

Surely not.

Good design

But this is a step forward, and now we can say, "I do not like it because it is too difficult to modify," or "I do not like it, because I do not know what the code is trying to do", but how to guide it forward?

If there are ways to describe the characteristics of bad design, and good design, and able to do this, it is not very good in an objective way it?

SOLID

In 2002, Robert Martin published his book Agile Software Development, Principles, Patterns, and Practices which describes the five principles of reusable software design, and call SOLID(Acronym) principles:

  • Single Responsibility Principle (Single Responsibility Principle)
  • Open / Closed Principle (Open / Closed Principle)
  • Richter substitution principle (Liskov Substitution Principle)
  • Interface Segregation Principle (Interface Segregation Principle)
  • Dependency Inversion Principle (Dependency Inversion Principle)

This book is a bit dated, the language it was a decade ago to discuss the language used. But maybe SOLIDsome aspects of the principles may provide some clues to us, Go program on how to talk about a well-designed.

Single Responsibility Principle (Single Responsibility Principle)

The first principle of SOLID, S, a single responsibility principle.

A class should have one, and only one, reason to change. – Robert C Martin

Go now apparently did not classses- on the contrary, we have a more powerful combination concept - but if you look at classthe usage of the word, I think that this time there will be some value.

Why only one piece of code changes important? Ah, just like your own code might change as frustrating detect changes in your code depends on the code occurs more pain in your feet. When your code must be changed, it should respond to direct stimulation to make changes, and should not become victims of collateral damage.

Therefore, the responsibility of a single reason code to modify the minimum.

Coupling & Cohesion

It describes how easy or difficult to change a software of two words: coupling and cohesion.

  • Coupling only one word, two variations of what is described with - a motion inducing another moving.
  • A related but separate concept of cohesion, a mutual attraction force.

In the context of software, mutual cohesion between the natural attraction characteristic described snippet.

Go To describe procedures for coupling and cohesion in the unit, we may talk about functions and methods, this discussion SRPis very common when, but I believe it starts with the Go package model.

SRP: Single Responsibility Principle

Package names

In Go, all of the code in a package in a well-designed package starting with its name. Package name which describes the use of both, also namespace prefix. Go package some excellent examples of standard library:

  • net/http - Provide http client and server
  • os/exec - execute external commands
  • encoding/json - achieve encoding and decoding JSON document

When you use symbols in another pakcage own internal, to use importthe statement, which builds coupling a source code level between the two package. They now know each other each other's existence.

Bad package names

This attention to the name is not pedantic. Named poor package if there really use, will lose the opportunity to set out its use.

  • serverWhat package offer? ..., ah, I hope that the service side, but which protocol to use it?
  • privateWhat package offer? I should not see things? It should have a common sign it?
  • commonpackage, and its companion utilspackage, as is often found with other 'partners' found together

We all like to see this package, it becomes a variety of garbage dump, because they have many responsibilities, so often there is no reason to change.

Go’s UNIX philosophy

In my opinion, if you do not mention Doug McIlroy of Unix philosophy, any discussion about decoupling design will be incomplete; small and sharp tools combine to solve the larger task, usually the original author can not imagine task.

I think Go package embodies the spirit of the Unix philosophy. In fact, every Go package itself is a small Go program, a single change unit, with a single responsibility.

Open / Closed Principle (Open / Closed Principle)

The second principle, namely O, is Bertrand Meyeropen / closed principle, he wrote in 1988:

Software entities should be open for extension, but closed for modification. – Bertrand Meyer, Object-Oriented Software Construction

The advice on how to apply to write 21 years later language?

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, there is a field year and a method Greet. We have the second type, which embeds a B A, because A is embedded, so the caller to see Method B covers the method A. Because embedded as a field A B, B can provide their own Greet method, a masking method A Greet.

But embedded not only apply to the method, you can also access an embedded type of field. As you can see, since A and B are defined in the same package, so you can access the B Field A private year, just as in B statement.

So embedded is a powerful tool that allows type Go open to expansion.

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 type of Cat, it can Legs method of calculating the number of its legs. We'll Cat type embedded in a new type OctoCat and declare Octocats have five legs. However, although OctoCat Legs defines its own method, the method returns 5, but when you call PrintLegs method, which returns 4.

This is because PrintLegs is defined on Cat type. It takes as its Cat receiver, it is sent to the Cat Legs method. Cat embedded type it does not know, and therefore can not change its method of embedding set.

Therefore, we can say that although the type Go open to extension but closed for modification.

In fact, the only method Go around syntactic sugar having a predetermined function formal parameters declared (i.e., a receiver) is.

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 what you pass it a function of the first argument, and because the Go does not support function overloading, OctoCat not a substitute for ordinary Cat. It makes me think of the next principle.

Richter substitution principle (Liskov Substitution Principle)

Richter substitution principle proposed by Barbara Liskov roughly that if both types of behavior shown by enabling the caller can not distinguish between these two types is the alternative.

Class-based language, Richter substitution principle is generally interpreted as having a specific subtypes regulate the various abstract base class. Go but no classes or inheritance, and therefore can not replace according to the abstract class hierarchy.

Interfaces

In contrast, the replacement range Go interface. In Go, you do not need to specify the type they implement a specific interface, but any type that implements the interface, as long as it has a method signature that matches the interface declaration.

We say in Go, the interface is implicitly rather than explicitly meet, which had a profound effect on how they are used in the language.

Well-designed interfaces are more likely to be smaller connector; popular approach is an interface contains only one method. Logically, small interfaces make it easy to achieve, otherwise it is difficult. Thus creating a package from a simple realization of the normal behavior of the composition.

io.Reader
type Reader interface {
        // Read reads up to len(buf) bytes into buf.
        Read(buf []byte) (n int, err error)
}
复制代码

This makes me very easy to think of my favorite Go interface io.Reader.

io.ReaderInterface is very simple; Readthe data is read into the buffer provided, any errors encountered during the number of bytes read and read and returned to the caller. It looks very simple, but very powerful.

Because io.Readerit can handle any expressed as a byte stream of things, so we can be created on almost anything Reader; constant string, byte array, standard input, network flow, gzip tar file, the command ssh remote execution of standard output .

And all of these implementations may be substituted for each other because they realize the same simple contract.

Therefore, for Go Richter substitution principle, it can be summed up by the late Jim Weirich's motto.

Require no more, promise no less. – Jim Weirich

Smooth transition "SOLID" fourth principle.

Interface Segregation Principle (Interface Segregation Principle)

The fourth principle is the principle of isolation interfaces, which reads as follows:

Clients should not be forced to depend on methods they do not use. –Robert C. Martin

In Go, the application interface segregation principle can refer to, isolation complete its required operating behavior of the process. As a specific example, suppose I have done 'to write a function to save disk structure Document' tasks.

// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) error
复制代码

I can define this function, let's call it Save, it will write to a given Document *os.File. But doing so will have some problems.

Save the signature rule out the option of writing data to a network location. Assume that the network storage needs that may later become the signature of this function must be changed, and affects all its caller.

Since the Savefile on disk directly operated, so the test is not convenient. To verify its operation, the test must be read after writing the contents of the file. In addition, the test must ensure that fwritten to a temporary location and then delete it.

*os.FileAnd also it defines a number of Saveindependent way, such as reading the directory and check if the path is a file link. If the Savesignature function can only describe *os.Filethe relevant part, it will be very practical.

How do we deal with these problems?

// Save writes the contents of doc to the supplied ReadWriterCloser.
func Save(rwc io.ReadWriteCloser, doc *Document) error
复制代码

Use io.ReadWriteCloserthe interface segregation principle we can apply to use more generic file type of interface to redefine Save.

This change implements any io.ReadWriteClosertype of interface may be used instead of the previous *os.File. Making Saveapplications more widely, and to Saveclarify the caller, *os.Filewhat type of method associated with the operation.

As Savea writer, I can no longer choose to call *os.Filethose methods are not relevant, because it is hidden in io.ReadWriteCloserthe back of the interface. We can further isolate the principle of using the interface.

First of all, if you Savefollow the principle of single responsibility, it is impossible to read the file you just wrote it to verify its contents - this should be the responsibility of another piece of code. Therefore, we can deliver to our Savestandard interfaces narrow, write and close only.

// Save writes the contents of doc to the supplied WriteCloser.
func Save(wc io.WriteCloser, doc *Document) error
复制代码

Secondly, to Saveprovide a mechanism to shut down its flow, we continue this mechanism to make it look like a file type of thing, which creates a problem, wcit will be shut down under what circumstances. SaveIt may be invoked unconditionally Close, or to call in case of success Close.

This gives Savethe caller poses a problem, because it may want to write the document after the other data written to the stream.

type NopCloser struct {
        io.Writer
}

// Close has no effect on the underlying writer.
func (c *NopCloser) Close() error { return nil }
复制代码

A rough solution is to define a new type, which is embedded a io.Writerand covers Closea method to prevent the Savemethod to close the underlying data stream.

But this may violate the Richter substitution principle, because NopCloser not actually shut down anything.

// Save writes the contents of doc to the supplied Writer.
func Save(w io.Writer, doc *Document) error
复制代码

A better solution is to redefine the Savereception only io.Writer, completely stripped of its addition to the data written to the stream responsibility to do anything.

Through the interface segregation principle application, our Save function, while on the demand side has been a most specific function - it only needs a writable parameters - and with the most common functions, we can use now Saveto save our data to any achieve io.Writerplaces.

A great rule of thumb for Go is accept interfaces, return structs. – Jack Lindamood

Even so, this sentence is an interesting memes in the past few years, it penetrates into the Go thought.

The Twitter-sized version of the lack of details, it's not Jack's fault, but I think it represents the first legitimate rational design of traditional Go

Dependency Inversion Principle (Dependency Inversion Principle)

The last principle is SOLID Dependency Inversion principle, the principle that:

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

However, Go programmers, dependency inversion of what it means in practice?

If you've applied all the principles we talked about before, your code should have been broken down into discrete packets, responsibility or purpose of each package has a well-defined. Your code should be based interface description of its dependencies, and should consider these interfaces are only required to describe the behavior of these functions. In other words, nothing should be done in addition.

So I think, in the context of Go's, Martin referring to the import graph structure.

In Go, import graph must be acyclic. Failure to comply with this requirement will result in a non-circulating compilation fails, but even more serious is that it represents a serious error in design.

In all of the same conditions, well-designed import graph Go program should be wide, relatively flat, rather than tall and narrow. If you have a package, it can not function without the aid operation in the case of another package, then this may indicate that the code is not well break down along pakcage border.

Dependency Inversion principle encourages you to specific areas of responsibility, to a higher level as far as possible along the import graph, or top-level push to the main package handlers, leaving lower-level code to handle abstract interface.

SOLID Go Design

Recall that when applied to Go, each SOLID principles are all about strong design statement, but together they have a central theme.

  • Single Responsibility Principle, encourage you to function, type, method structured bag with a cohesive nature; type belong to each other, the function to serve a single purpose.
  • Open / closed principle, will encourage you to use a combination of embedded simple types into more complex types.
  • Richter substitution principle, encourage you to express dependencies between packages according to the interface rather than a specific type. By defining a small interface, we can be more confident that implementation will faithfully fulfill their contract.
  • Interface segregation principle, further adopted this idea and encourages you to define only depend on the functions and methods they need to act. Method if your function only requires a single interface type of the parameter, the function is more likely to be a liability.
  • Dependency Inversion principle, encourage you to follow the timing compile time to run from the time of the transfer package relies on knowledge. In Go, the number of import statements, we can reduce use by a specific package to see this.

If you want to sum up this lecture, it is probably this: interfaces let you apply the SOLID principles to Go programs.

Because the interface allows programmers to describe their Go package offers what - not how to do it. Another way is the "decoupling", which is really the goal, because the more loosely coupled software more easily modified.

As Sandi Metz said:

Design is the art of arranging code that needs to work today, and to be easy to change forever. – Sandi Metz

If you want to be because the Go language long-term investment, maintainability Go program, it is easier to change will be a key factor in their decisions.

end

Finally, let us go back to my question open this speech; how many Go programmers the world? This is my guess:

By 2020, there will be 500,000 Go developers. - me

What 500 000 Go programmers would do with their time? Well, obviously, they would write a lot of code Go, tell the truth, not all are good code, some will be very bad.

Please understand, I say not so cruel, but, in this room, every other person with experience in the development of language - the language from you, Go-- came to know from your own experience, this prophecy thing is really.

Within C++, there is a much smaller and cleaner language struggling to get out. – Bjarne Stroustrup, The Design and Evolution of C++

All programmers have the opportunity to let our language success depend on our collective ability to bring people do not start talking about Go mess of things, just as they are today for C ++ joke.

Mocking description of other languages ​​too long, too complicated and lengthy, will one day be turned to GO, I do not want to see that happen, so I have a request.

Go programmers need to talk less about the framework, to talk about design. We need to stop focusing on performance at all costs, in turn, go all out to focus on reuse.

I want to see people talking about how to use the language we use today, regardless of their choices and limitations, design solutions and solve practical problems.

I want to hear people talking about how well designed, decoupling, reuse, most importantly, respond to changes in fashion design Go program.

… one more thing

Today, all of you can hear presentations from many speakers, that's great, but the fact is that no matter how large the conference, compared with the number of people using the Go Go life cycle, we are only a small part.

Therefore, we need to tell how the rest of the world should write good software. Excellent software, a combination of software, the software is easy to change, and show them how to use Go make changes. Start with your.

I hope you start talking about design, perhaps using some of the ideas I put forward here, I hope you can do your own research, and apply these ideas to your project. I want you to:

  • Write a blog article about the design.
  • Teach a workshop about the design.
  • Write a book about what you've learned of.
  • Next year and then return to this conference, you talk about the achievements made.

Because by doing these things, we can build a culture of Go developers, they are designed for long-lasting care program.

Thank you.

Original: SOLID Go Design

Guess you like

Origin juejin.im/post/5d4a4339e51d4561a60d9d85