官术网_书友最值得收藏!

Single responsibility principle (SRP)

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

Go doesn't have classes, but if we squint a little and replace the word class with objects (structs, functions, interfaces or packages), then the concept still applies.

Why do we want our objects to do only one thing? Let's look at a couple of objects that do one thing:

These objects are simple and easy to use, and have a wide range of uses.

Designing objects so that they all do only one thing sounds okay in the abstract. But you are probably thinking that doing so for an entire system would add a lot more code. Yes, it will. However, what it doesn't do is add complexity; in fact, it significantly reduces it. Each piece of code would be smaller and easier to understand, and therefore easier to test. This fact gives us the first advantage of SRP:

SRP reduces the complexity by decomposing code into smaller, more concise pieces

With a name like single responsibility principle, it would be safe to assume that it is all about responsibility, but so far, all we have talked about is change. Why is this? Let's look at an example:

// Calculator calculates the test coverage for a directory 
// and it's sub-directories
type Calculator struct {
// coverage data populated by `Calculate()` method
data map[string]float64
}

// Calculate will calculate the coverage
func (c *Calculator) Calculate(path string) error {
// run `go test -cover ./[path]/...` and store the results
return nil
}

// Output will print the coverage data to the supplied writer
func (c *Calculator) Output(writer io.Writer) {
for path, result := range c.data {
fmt.Fprintf(writer, "%s -> %.1f\n", path, result)
}
}

The code looks reasonable—one member variable and two methods. It does not, however, conform to SRP. Let's assume that the app was successful, and we decided that we also needed to output the results to CSV. We could add a method to do that, as shown in the following code:

// Calculator calculates the test coverage for a directory 
// and it's sub-directories
type Calculator struct {
// coverage data populated by `Calculate()` method
data map[string]float64
}

// Calculate will calculate the coverage
func (c *Calculator) Calculate(path string) error {
// run `go test -cover ./[path]/...` and store the results
return nil
}

// Output will print the coverage data to the supplied writer
func (c Calculator) Output(writer io.Writer) {
for path, result := range c.data {
fmt.Fprintf(writer, "%s -> %.1f\n", path, result)
}
}

// OutputCSV will print the coverage data to the supplied writer
func (c Calculator) OutputCSV(writer io.Writer) {
for path, result := range c.data {
fmt.Fprintf(writer, "%s,%.1f\n", path, result)
}
}

We have changed the struct and added another Output() method. We have added more responsibilities to the struct and, in doing so, we have added complexity. In this simple example, our changes are confined to one method, so there's no risk that we broke the previous code. However, as the struct gets bigger and more complicated, our changes are unlikely to be so clean.

Conversely, if we were to break the responsibilities into Calculate and Output, then adding more outputs would mere define new structs. Additionally, should we decide that we don't like the default output format, we could change it separately from other parts.

Let's try a different implementation:

// Calculator calculates the test coverage for a directory 
// and it's sub-directories
type Calculator struct {
// coverage data populated by `Calculate()` method
data map[string]float64
}

// Calculate will calculate the coverage
func (c *Calculator) Calculate(path string) error {
// run `go test -cover ./[path]/...` and store the results
return nil
}

func (c *Calculator) getData() map[string]float64 {
// copy and return the map
return nil
}

type Printer interface {
Output(data map[string]float64)
}

type DefaultPrinter struct {
Writer io.Writer
}

// Output implements Printer
func (d *DefaultPrinter) Output(data map[string]float64) {
for path, result := range data {
fmt.Fprintf(d.Writer, "%s -> %.1f\n", path, result)
}
}

type CSVPrinter struct {
Writer io.Writer
}

// Output implements Printer
func (d *CSVPrinter) Output(data map[string]float64) {

for path, result := range data {
fmt.Fprintf(d.Writer, "%s,%.1f\n", path, result)
}
}

Do you notice anything significant about the printers? They have no connection at all to the calculation. They could be used for any data in the same format. This leads to the second advantage of SRP:

SRP increases the potential reusability of code.

In the first implementation of our coverage calculator, to test the Output() method we would be first call the Calculate() method. This approach increases the complexity of our tests by coupling the calculation with the output. Consider the following scenarios:

  • How do we test for no results?
  • How do we test edge conditions, such as 0% or 100% coverage?

After decoupling these responsibilities, we should encourage ourselves to consider the inputs and outputs of each part in a less interdependent manner, hence making the tests easier to write and maintain. This leads to the third advantage of SRP:

SRP makes tests simpler to write and maintain.

SRP is also an excellent way to improve general code readability. Take a look at this next example:

func loadUserHandler(resp http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}
userID, err := strconv.ParseInt(req.Form.Get("UserID"), 10, 64)
if err != nil {
resp.WriteHeader(http.StatusPreconditionFailed)
return
}

row := DB.QueryRow("SELECT * FROM Users WHERE ID = ?", userID)

person := &Person{}
err = row.Scan(&person.ID, &person.Name, &person.Phone)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}

encoder := json.NewEncoder(resp)
encoder.Encode(person)
}

I'd bet that took more than five seconds to understand. How about this code?

func loadUserHandler(resp http.ResponseWriter, req *http.Request) {
userID, err := extractIDFromRequest(req)
if err != nil {
resp.WriteHeader(http.StatusPreconditionFailed)
return
}

person, err := loadPersonByID(userID)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}

outputPerson(resp, person)
}

By applying SRP at the function level, we have reduced the function's bloat and increased its readability. The function's single responsibility is now to coordinate the calls to the other functions.

主站蜘蛛池模板: 通许县| 长丰县| 涟源市| 渝北区| 道真| 柳林县| 灵川县| 从化市| 马山县| 会昌县| 西峡县| 平塘县| 景泰县| 乌鲁木齐县| 明星| 神木县| 涡阳县| 东丰县| 邮箱| 灵武市| 尼玛县| 丰顺县| 潮安县| 敖汉旗| 苍溪县| 荆门市| 呼和浩特市| 澳门| 方山县| 合肥市| 西盟| 金门县| 永宁县| 大安市| 武冈市| 吴旗县| 乐亭县| 尤溪县| 仁化县| 嘉禾县| 山阳县|