[译]Golang中的依赖注入

[译]Golang中的依赖注入

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

我最近使用Go建立了一个小项目,因为最近几年一直用Java,我马上就被Go语言生态里依赖注入(DI)的缺失震惊了。我决定尝试用Uber的dig库来建立个人项目,结果很是不错。git

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

DI概述

依赖注入是这样一个概念,你的组件(在go中一般是structs)应该在被建立时接收到它们的依赖。这与组件初始化过程当中建立本身的依赖反面模式恰好相反。让咱们来看一个例子。golang

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

type Server struct {
  config *Config
}

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

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

然而这样也有些缺点。首先,若是咱们想改变Config建立的方式,咱们必须修改全部调用了它的建立方法的代码。假设,举个例子,咱们的buildMyConfigSomehow函数如今须要一个参数,每一个调用将必需访问那个参数,且须要将它传递给buildMyConfigSomehow方法。json

并且,mock Config会变得很棘手。咱们必须经过某种方式去触及到New函数的内部,去鼓捣Config的建立。服务器

下面是使用DI的方式:app

type Server struct {
  config *Config
}

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

如今Server的建立与Config的建立解除了耦合,咱们能够随意选择建立Config的逻辑,而后将结果数据传递给New方法。框架

并且,若是Config是个接口,mock就会变得更容易,只要实现了接口,均可传递给New方法。这使咱们对Server进行mock变得容易。ide

主要的负面影响是,咱们在建立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"`
}

一个PersonIdNameAge,没别的了。

接下来,咱们看一下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。

相关文章
相关标签/搜索