分享
  1. 首页
  2. 文章

[译]Golang中的依赖注入

AlexTuan · · 3246 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

[译]Golang中的依赖注入

文章来源:Dependency Injection in Go
关于作者:Drew Olson
作者博客:software is fun
译者按:本文用于介绍DI和golang中DI库dig的简单使用,适合对go有一定了解的开发者。

我最近使用Go创建了一个小项目,由于最近几年一直用Java,我立刻就被Go语言生态里依赖注入(DI)的缺失震惊了。我决定尝试用Uber的dig库来创建我的项目,结果非常不错。

我发觉DI帮我解决了很多在以前的Go应用中遇到的问题——init函数的过度使用,全局变量的滥用和复杂的应用初始化设置。
在这篇文章中,我会介绍DI,并展示一个应用在使用DI前后的区别(使用dig库)。

DI概述

依赖注入是这样一个概念,你的组件(在go中通常是structs)应该在被创建时接收到它们的依赖。这与组件初始化过程中创建自己的依赖反面模式刚好相反。让我们来看一个例子。

假设你有一个Server结构,要实现其功能,需要一个Config结构。一种办法是在Server初始化时,构建它自己的Config

type Server struct {
 config *Config
}
func New() *Server {
 return &Server{
 config: buildMyConfigSomehow(),
 }
}

这样看上去很方便,调用者甚至不需要知道我们的Server需要访问Config。这些对函数的使用者来说都是隐藏的。

然而这样也有些缺点。首先,如果我们想改变Config创建的方式,我们必须修改所有调用了它的创建方法的代码。假设,举个例子,我们的buildMyConfigSomehow函数现在需要一个参数,每个调用将必需访问那个参数,且需要将它传递给buildMyConfigSomehow方法。

而且,mock Config会变得很棘手。我们必须通过某种方式去触及到New函数的内部,去鼓捣Config的创建。

下面是使用DI的方式:

type Server struct {
 config *Config
}
func New(config *Config) *Server {
 return &Server{
 config: config,
 }
}

现在Server的创建与Config的创建解除了耦合,我们可以随意选择创建Config的逻辑,然后将结果数据传递给New方法。

而且,如果Config是个接口,mock就会变得更容易,只要实现了接口,都可传递给New方法。这使我们对Server进行mock变得容易。

主要的负面影响是,我们在创建Server前必须手动创建Config,这一点比较痛苦。我们创建了一个"依赖图"——我们必须首先创建Config因为Server依赖它。在现实的应用中,依赖图可能变得非常大,这会导致创建所有你的应用需要的组件的逻辑非常复杂。

这正是DI框架能提供帮助的地方。一个DI框架一般有两个主要功用:

  1. 一个"提供"新组件的机制。笼统地说,这个机制告诉DI框架你需要哪些其他组件,来创建你自己(译者按:可以理解为你的一个程序),以及有了这些组件之后如何进行创建。
  2. 一个用于"取用"创建好的组件的机制。

一个DI框架一般基于你告诉框架的"提供者"来创建一个图(graph),并且决定如何创建你的object。抽象的概念比较难以理解,所以让我们看一个合适的例子。

一个示例应用

我们将要看一段代码,一个在接收到客户端GET people请求时,会发送一个JSON响应的HTTP服务器。简单起见,我们吧代码都放在main包里,不过现实中千万别这么干。全部的实例代码可以在此处下载。

首先,让我们看一下Person结构,除了一些JSON tag之外,它没有任何行为定义。

type Person struct {
 Id int `json:"id"`
 Name string `json:"name"`
 Age int `json:"age"`
}

一个PersonId,NameAge,没别的了。

接下来,我们看一下Config,类似于Person,它没有任何依赖,不同于Person的是,我们将提供一个构造器(constructor)。

type Config struct {
 Enabled bool
 DatabasePath string
 Port string
}
func NewConfig() *Config {
 return &Config{
 Enabled: true,
 DatabasePath: "./example.db",
 Port: "8000",
 }
}

Enabled控制应用是否应该返回真实数据。DatabasePath表示数据库位置(我们使用sqlite)。Port表示服务器运行时监听的端口。

这个函数用于打开数据库连接,它依赖于Config并返回一个*sql.DB

func ConnectDatabase(config *Config) (*sql.DB, error) {
 return sql.Open("sqlite3", config.DatabasePath)
}

接下来我们会看到PersonRepository。这个结构负责从数据库中获取people数据并将其反序列化,保存到Person结构当中。

type PersonRepository struct {
 database *sql.DB
}
func (repository *PersonRepository) FindAll() []*Person {
 rows, _ := repository.database.Query(
 `SELECT id, name, age FROM people;`
 )
 defer rows.Close()
 people := []*Person{}
 for rows.Next() {
 var (
 id int
 name string
 age int
 )
 rows.Scan(&id, &name, &age)
 people = append(people, &Person{
 Id: id,
 Name: name,
 Age: age,
 })
 }
 return people
}
func NewPersonRepository(database *sql.DB) *PersonRepository {
 return &PersonRepository{database: database}
}

PersonRepository需要一个待创建的数据库连接。它暴露一个叫做FindAll的函数,这个函数使用数据库连接,并返回一个包含数据库数据的Person顺序表。

为了在HTTP Server和PersonRepository中间新增一层,我们将会创建一个PersonService

type PersonService struct {
 config *Config
 repository *PersonRepository
}
func (service *PersonService) FindAll() []*Person {
 if service.config.Enabled {
 return service.repository.FindAll()
 }
 return []*Person{}
}
func NewPersonService(config *Config, repository *PersonRepository)
*PersonService {
 return &PersonService{config: config, repository: repository}
}

我们的PersonService依赖于ConfigPersonRepository。它暴露一个叫做FindAll的函数,根据应用是否是enabled去调用PersonRepository

最后,我们来到Server,负责运行一个HTTP Server,并且把请求分配到PersonService处理。

type Server struct {
 config *Config
 personService *PersonService
}
func (s *Server) Handler() http.Handler {
 mux := http.NewServeMux()
 mux.HandleFunc("/people", s.people)
 return mux
}
func (s *Server) Run() {
 httpServer := &http.Server{
 Addr: ":" + s.config.Port,
 Handler: s.Handler(),
 }
 httpServer.ListenAndServe()
}
func (s *Server) people(w http.ResponseWriter, r *http.Request) {
 people := s.personService.FindAll()
 bytes, _ := json.Marshal(people)
 w.Header().Set("Content-Type", "application/json")
 w.WriteHeader(http.StatusOK)
 w.Write(bytes)
}
func NewServer(config *Config, service *PersonService) *Server {
 return &Server{
 config: config,
 personService: service,
 }
}

Server依赖于PersonServiceConfig
好了,我们现在知道我们系统的所有组件了。现在怎么他妈的初始化它们并且启动我们的系统?

令人害怕的main()

func main() {
 config := NewConfig()
 db, err := ConnectDatabase(config)
 if err != nil {
 panic(err)
 }
 personRepository := NewPersonRepository(db)
 personService := NewPersonService(config, personRepository)
 server := NewServer(config, personService)
 server.Run()
}

我们首先创建Config,然后使用Config创建数据库连接。接下来我们可以创建PersonRepository,这样就可以创建PersonService了,最后,我们使用这些来创建Server并启动它。

呼,刚才那操作太复杂了。更糟糕的是,当我们的应用变得更复杂时,我们的main的复杂度会持续增长。每次我们的任何一个组件新增一个依赖,为了创建这个组件,我们都不得不琢磨这个依赖在main中的顺序和逻辑。
所以你也许会猜,一个依赖注入框架可以帮我们解决这个问题,我们来试一下怎么用。

创建一个容器

"容器(container)"这个属于在DI框架中,通常表示你存放"提供者(provider)"和获取创建好的对象的一个地方。
dig库提供给我们Provide函数,用于添加我们自己的提供者;还有Invoke函数,用于从容器中获取创建好的对象。

首先,我们创建一个新的容器。

container := dig.New()

现在我们可以添加新的提供者,方法很简单,我们用container调用Provide函数,这个函数接收一个参数:一个函数。这个参数函数可以有任意数量的参数(代表将要创建的组件的依赖)以及一个或两个返回值(代表这个函数提供的组件,以及一个可选的error)。

container.Provide(func() *Config {
 return NewConfig()
})

上述代码表示,"我提供一个Config给容器,为了创建它,我并不需要其他东西。"。现在我们已经告诉了容器如何创建一个Config类型,我们可以使用它来创建其他类型了。

container.Provide(func(config *Config) (*sql.DB, error) {
 return ConnectDatabase(config)
})

这部分代码表示,"我提供一个*sql.DB类型给容器,为了创建它,我需要一个Config,我可能会返回一个可选的error"。
在这两个例子中,我们有点啰嗦了,因为我们已经定义了NewConfigConnectDatabase函数,我们可以直接将它们作为提供者给容器使用。

container.Provide(NewConfig)
container.Provide(ConnectDatabase)

现在,我们可以直接向容器请求一个构造好的组件,任何我们给出了提供者的类型都可以。我们用Invoke函数来做这个操作。Invoke函数接收一个参数——一个接收任意数量参数的函数,这些参数正是我们想要容器为我们创建的组件。

container.Invoke(func(database *sql.DB) {
 // sql.DB is ready to use here
})

容器的操作很聪明,它是这样做的:

* 容器识别到我们在请求一个`*sql.DB`
* 它发现我们的函数`ConnectDatabase`提供了这种类型
* 接下来它发现`ConnectDatabase`依赖一个`Config`
* 它会找到`Config`的提供者,`NewConfig`函数
* `NewConfig`函数没有任何依赖,于是被调用 
* `NewConfig`的返回值是一个`Config`,被作为参数传递给`ConnectDatabase`
* `ConnectionData`的结果是一个`*sql.DB`,被传回给`Invoke`的调用者

容器为我们做了很多事,实际上它做的更多。容器只会为每种类型提供一个实例,这意味着我们永远不会意外的创建了第二个数据库连接,即使我们在多个地方调用(比如多个repository)。

一个更好的main()

现在我们知道dig容器是如何工作的了,让我们用它来创建一个更好的main。

func BuildContainer() *dig.Container {
 container := dig.New()
 container.Provide(NewConfig)
 container.Provide(ConnectDatabase)
 container.Provide(NewPersonRepository)
 container.Provide(NewPersonService)
 container.Provide(NewServer)
 return container
}
func main() {
 container := BuildContainer()
 err := container.Invoke(func(server *Server) {
 server.Run()
 })
 if err != nil {
 panic(err)
 }
}

除了Invokeerror返回,其它的我们都见过了。如果任何被Invoke使用的提供者返回错误,对Invoke的调用就会停止,并返回error。
即使这个例子很小,应该也足以看出这种方法相对于"标准"main的一些好处了。随着我们的应用规模的增长,好处也会更多更明显。
其中一个最重要的好处就是解耦组件的创建和依赖的创建。比方说,我们的PersonRepository需要访问Config,我们只需要修改构造器NewPersonRepository,给它加上一个Config参数,其它什么都不用变。
也有一些其它的好处,减少了全局的状态,减少了init的调用(依赖会在被需要时延迟创建,且只会创建一次,避免了有报错倾向的init调用),让独立组件的测试更加容易。想象下创建一个容器,去请求一个创建好的对象做测试,或者mock所有的依赖并创建对象。所有的这些都会随着DI的引入变得容易。

一个值得推广的想法

我相信依赖注入帮助我们创建更健壮,更易测试的程序。随着应用程序规模的增长,这一点也更加颠扑不破。Go非常适合创建大型的应用程序,并且dig是一个很好的DI工具。我认为Go社区应该接受DI并且在尽可能多的应用程序中使用它。

更新

Google最近发布了它们自己的DI容器,叫做wire。它通过代码生成的方式避免了运行时反射。比起dig我更建议用wire。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:AlexTuan

查看原文:[译]Golang中的依赖注入

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
3246 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏