Skip to content

Commit

Permalink
整理dependency injection章节
Browse files Browse the repository at this point in the history
  • Loading branch information
william's mac committed Nov 20, 2019
1 parent 18302de commit 0a978bb
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 83 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
6. [结构体struct、方法method和接口interface](structs-methods-and-interfaces.md) - 学习结构体`struct`, 方法methods, 接口`interface`和表驱动测试.
7. [指针Pointers和错误errors](pointers-and-errors.md) - 学习指针和错误。
8. [字典Maps](maps.md) - 学习如何在字典数据结构中存储值。
9. [Dependency Injection](dependency-injection.md) - Learn about dependency injection, how it relates to using interfaces and a primer on io.
9. [依赖注入DI](dependency-injection.md) - 学习依赖注入, 它和接口的关系, 简单介绍io包。
10. [Mocking](mocking.md) - Take some existing untested code and use DI with mocking to test it.
11. [Concurrency](concurrency.md) - Learn how to write concurrent code to make your software faster.
12. [Select](select.md) - Learn how to synchronise asynchronous processes elegantly.
Expand Down
135 changes: 60 additions & 75 deletions dependency-injection.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
# Dependency Injection
# 依赖注入

**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/di)**
**[本章代码](https://github.com/spring2go/learn-go-with-tests/tree/master/di/v1)**

It is assumed that you have read the structs section before as some understanding of interfaces will be needed for this.
学习本章之前,我们假定你已经阅读过结构体(structs)章节,因为要理解依赖注入必须先理解接口。

There is _a lot_ of misunderstandings around dependency injection around the programming community. Hopefully, this guide will show you how
在编程社区里,大家对依赖注入有很多误解。希望本章可以澄清:

* You don't need a framework
* It does not overcomplicate your design
* It facilitates testing
* It allows you to write great, general-purpose functions.
* 依赖注入并不一定需要框架
* 它也不会让你的设计变复杂
* 它有助于测试
* 它能帮助你写出通用的函数

We want to write a function that greets someone, just like we did in the hello-world chapter but this time we are going to be testing the _actual printing_.
我们想要写一个问候某人的函数,正如我们之前在hello-world章节做的那样,但是这次我们要测试的是**实际打印输出的部分**

Just to recap, here is what that function could look like
简单回忆一下,这个函数应该长成这样:

```go
func Greet(name string) {
fmt.Printf("Hello, %s", name)
}
```

But how can we test this? Calling `fmt.Printf` prints to stdout, which is pretty hard for us to capture using the testing framework.
该如何测试呢?`fmt.Printf`会打印到控制台,但是我们很难捕获控制台的输出,然后用测试框架对其进行测试。

What we need to do is to be able to **inject** \(which is just a fancy word for pass in\) the dependency of printing.
我们希望能够注入(**inject**)打印依赖,这里的注入其实就是传入。

**Our function doesn't need to care **_**where**_** or **_**how**_** the printing happens, so we should accept an **_**interface**_** rather than a concrete type.**
我们的函数并不需要关心打印发生在哪里,或者是如何打印的,所以我们应当接受一个接口,而不是一个具体类型。

If we do that, we can then change the implementation to print to something we control so that we can test it. In "real life" you would inject in something that writes to stdout.
如果我们使用接口的话,我们就可以改变具体实现。在测试的时候,用一种可控的能够测试的实现。在真实环境中,再换成另外一种可以输出到控制台的实现。

If you look at the source code of `fmt.Printf` you can see a way for us to hook in
如果你看下`fmt.Printf`的源码,你可以学习到这种让我们可以hook in具体实现的方式:

```go
// It returns the number of bytes written and any write error encountered.
Expand All @@ -38,9 +38,9 @@ func Printf(format string, a ...interface{}) (n int, err error) {
}
```

Interesting! Under the hood `Printf` just calls `Fprintf` passing in `os.Stdout`.
有意思的是,`Printf`底层只是调用了`Fprintf`,并传入了一个`os.Stdout`

What exactly _is_ an `os.Stdout`? What does `Fprintf` expect to get passed to it for the 1st argument?
`os.Stdout`到底是什么?`Fprintf`期望传入的第一个参数到底是什么?

```go
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
Expand All @@ -52,19 +52,21 @@ func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
}
```

An `io.Writer`
实际上是一个 `io.Writer`

```go
type Writer interface {
Write(p []byte) (n int, err error)
}
```

As you write more Go code you will find this interface popping up a lot because it's a great general purpose interface for "put this data somewhere".
随着你写的Go语言代码越来越多,你经常会碰到这个接口,因为它是一个通用接口,表示:"将数据写到某处"。

So we know under the covers we're ultimately using `Writer` to send our greeting somewhere. Let's use this existing abstraction to make our code testable and more reusable.
现在你知道,在底层你最终会使用`Writer`将我们的问候语发送到某处。现在我们可以利用这种抽象,让我们的代码变得易于测试和重用。

## Write the test first
## 先写测试

[`di_test.go`](https://github.com/spring2go/learn-go-with-tests/blob/master/di/v1/di_test.go)

```go
func TestGreet(t *testing.T) {
Expand All @@ -80,63 +82,43 @@ func TestGreet(t *testing.T) {
}
```

The `buffer` type from the `bytes` package implements the `Writer` interface.

So we'll use it in our test to send in as our `Writer` and then we can check what was written to it after we invoke `Greet`

## Try and run the test
来自`bytes`包的`buffer`类型实现`Writer`接口。

The test will not compile

```text
./di_test.go:10:7: too many arguments in call to Greet
have (*bytes.Buffer, string)
want (string)
```
在该测试中,我们让这个`buffer`成为我们的`Writer`,在调用`Greet`之后,我们就可以检查其中的内容。

## Write the minimal amount of code for the test to run and check the failing test output
## 写程序逻辑

_Listen to the compiler_ and fix the problem.

```go
func Greet(writer *bytes.Buffer, name string) {
fmt.Printf("Hello, %s", name)
}
```

`Hello, Chris di_test.go:16: got '' want 'Hello, Chris'`

The test fails. Notice that the name is getting printed out, but it's going to stdout.

## Write enough code to make it pass

Use the writer to send the greeting to the buffer in our test. Remember `fmt.Fprintf` is like `fmt.Printf` but instead takes a `Writer` to send the string to, whereas `fmt.Printf` defaults to stdout.
类型`bytes.Buffer`是实现`Writer`接口的,这样我们就可以把问候语输出到一个`buffer`中。`fmt.Fprintf``fmt.Printf`类似,只是`fmt.Fprintf`接受的参数是一个`Writer`,而`fmt.Printf`缺省输出到标准控制台(stdout)。

```go
func Greet(writer *bytes.Buffer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}
```

The test now passes.
现在测试可以通过。

## Refactor
## 重构

Earlier the compiler told us to pass in a pointer to a `bytes.Buffer`. This is technically correct but not very useful.
上面的实现要求我们传一个`bytes.Buffer`类型的指针,这种做法技术上正确,但并不通用。

To demonstrate this, try wiring up the `Greet` function into a Go application where we want it to print to stdout.
为了演示这个问题,可以尝试在主程序中调用一次`Greet`函数,传入一个`os.Stdout`:

```go
func main() {
Greet(os.Stdout, "Elodie")
}
```

运行`go run di.go`会看到如下错误提示:

`./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet`

As discussed earlier `fmt.Fprintf` allows you to pass in an `io.Writer` which we know both `os.Stdout` and `bytes.Buffer` implement.
正如之前提到过的,`fmt.Fprintf`允许我们传入`io.Writer`接口,我们也知道`os.Stdout``bytes.Buffer`都实现这个接口。

如果我们修改代码,使用更通用的接口,那么我们的测试和主程序就都可以通过了。

If we change our code to use the more general purpose interface we can now use it in both tests and in our application.
[`di.go`](https://github.com/spring2go/learn-go-with-tests/blob/master/di/v1/di.go)

```go
package main
Expand All @@ -156,13 +138,15 @@ func main() {
}
```

## More on io.Writer
## 关于io.Writer的更多内容

What other places can we write data to using `io.Writer`? Just how general purpose is our `Greet` function?
我们用`io.Writer`还可以将数据写到其它地方去吗?我们的`Greet`函数有多通用?

### The internet
### Web服务器

Run the following
运行下面的代码:

[`di.go`](https://github.com/spring2go/learn-go-with-tests/blob/master/di/v2/di.go)

```go
package main
Expand All @@ -186,32 +170,33 @@ func main() {
}
```

Run the program and go to [http://localhost:5000](http://localhost:5000). You'll see your greeting function being used.
然后浏览器访问[http://localhost:5000](http://localhost:5000),可以看到`Greet`函数输出在网页中。

后面章节我们会进一步讲HTTP服务器,所以目前不必纠结实现细节。

HTTP servers will be covered in a later chapter so don't worry too much about the details.
当你创建一个HTTP handler,入参有一个`http.ResponseWriter`和一个`http.Request`,其中`http.ResponseWriter`用于输出内容,`http.Request`用于获取用户请求。在我们的实现中,我们将问候语写入`http.ResponseWriter`实例。

When you write an HTTP handler, you are given an `http.ResponseWriter` and the `http.Request` that was used to make the request. When you implement your server you _write_ your response using the writer.
你可能已经猜到了,`http.ResponseWriter`也是实现`io.Writer`接口的,所以我们可以在handler中重用`Greet`函数。

You can probably guess that `http.ResponseWriter` also implements `io.Writer` so this is why we could re-use our `Greet` function inside our handler.
## 总结

## Wrapping up
刚开始我们的代码不太好测试,因为我们把数据写到不受我们控制的地方。

Our first round of code was not easy to test because it wrote data to somewhere we couldn't control.
为了让代码易于测试,我们重构了代码,使用依赖注入的方式,让我们可以控制数据写到何处。**依赖注入(dependency injection)**让我们:

_Motivated by our tests_ we refactored the code so we could control _where_ the data was written by **injecting a dependency** which allowed us to:
* **易于测试代码**,如果某个函数不易测试,通常是因为函数中硬编码了某种依赖或全局状态。如果我们的服务层使用了一个全局的数据库连接池,那么测试就不太容易,测起来也很慢。DI让我们能够注入数据库依赖(通过接口),然后你可以mock掉某种在测试中无法控制的依赖。
* **关注分离(Separation of concerns)**,将**数据的去处****如何产生数据**两者进行解耦。如果你感觉一个方法/函数承担了太多职责(例如既产生数据也写到数据库,或者既处理HTTP请求也做业务逻辑处理),那么DI可能是你需要利用的工具。
* **让代码易于重用**,首先是让代码可以在测试中重用,进一步如果想尝试某种新的实现,你就可以采用DI将新实现作为依赖进行注入。

* **Test our code** If you can't test a function _easily_, it's usually because of dependencies hard-wired into a function _or_ global state. If you have a global database connection pool for instance that is used by some kind of service layer, it is likely going to be difficult to test and they will be slow to run. DI will motivate you to inject in a database dependency \(via an interface\) which you can then mock out with something you can control in your tests.
* **Separate our concerns**, decoupling _where the data goes_ from _how to generate it_. If you ever feel like a method/function has too many responsibilities \(generating data _and_ writing to a db? handling HTTP requests _and_ doing domain level logic?\) DI is probably going to be the tool you need.
* **Allow our code to be re-used in different contexts** The first "new" context our code can be used in is inside tests. But further on if someone wants to try something new with your function they can inject their own dependencies.
### 关于Mock测试

### What about mocking? I hear you need that for DI and also it's evil
后续章节我们会涉及Mocking。你可以用mock来替代代码功能,运行测试时,注入一个mock(假的)版本,这个mock是你可以控制和测试的。

Mocking will be covered in detail later \(and it's not evil\). You use mocking to replace real things you inject with a pretend version that you can control and inspect in your tests. In our case though, the standard library had something ready for us to use.
### 关于Go语言标准库

### The Go standard library is really good, take time to study it
对于`io.Writer`有一些熟悉之后,你就学会了在测试中可以使用`bytes.Buffer`作为`Writer`,然后在一个命令行程序或者一个Web服务器中,可以使用其它的`Writer`实现。

By having some familiarity with the `io.Writer` interface we are able to use `bytes.Buffer` in our test as our `Writer` and then we can use other `Writer`s from the standard library to use our function in a command line app or in web server.
随着你对Go语言标准库的熟悉,你会越来越多的见到这类通用的接口。然后你可以在你的代码中,通过接口尽量重用标准库的功能,同时让你的软件更易于重用。

The more familiar you are with the standard library the more you'll see these general purpose interfaces which you can then re-use in your own code to make your software reusable in a number of contexts.
本章案例参考了书籍[Go程序设计语言](http://product.dangdang.com/25072202.html),如果你想学习更多,推荐购买这本Go语言的经典书。

This example is heavily influenced by a chapter in [The Go Programming language](https://www.amazon.co.uk/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440), so if you enjoyed this, go buy it!
8 changes: 4 additions & 4 deletions di/v1/di_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import (

func TestGreet(t *testing.T) {
buffer := bytes.Buffer{}
Greet(&buffer, "Chris")
Greet(&buffer, "Bobo")

got := buffer.String()
want := "Hello, Chris"
expected := "Hello, Bobo"

if got != want {
t.Errorf("got %q want %q", got, want)
if got != expected {
t.Errorf("got %q expected %q", got, expected)
}
}
6 changes: 3 additions & 3 deletions di/v2/di_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ func TestGreet(t *testing.T) {
Greet(&buffer, "Chris")

got := buffer.String()
want := "Hello, Chris"
expected := "Hello, Chris"

if got != want {
t.Errorf("got %q want %q", got, want)
if got != expected {
t.Errorf("got %q expected %q", got, expected)
}
}

0 comments on commit 0a978bb

Please sign in to comment.