[譯]Golang中的依賴注入

來源:互聯網
上載者:User

[譯]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"`}

一個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。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.