這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
依賴注入(DI)是一種解耦組件之間依賴關係的設計模式。在需要的時候,不同組件之間可以通過一個統一的介面擷取其它組件中的對象和狀態。Go語言的介面設計,避免了很多需要使用第三方依賴注入架構的情況(比如Java,等等)。我們的注入方案只提供非常少的類似Dager或Guice中的注入方案,而專註於盡量避免手動去設定物件和組件之間的依賴關係。因為,我們認為如果在Go程式碼程式庫中,注入能夠更加容易理解,就根本沒有必要那樣。 在Go中實現注入只需要這幾個簡單的步驟: |
晏雨涵 翻譯於 1 個月 前 0人頂 頂 翻譯的不錯哦! |
全域變數 先從一個一致的、崇高的目標開始,我們需要一些如Mongo、Memcache等服務的全域連線物件。大致是這樣的: var MongoService mongo.Service func InitMongoService(url string) { MongoService = ...} func GetApp(id uint64) *App { a := new(App) MongoService.Session().Find(..).One(a) return a} 通常 main() 函數會調用配置在flags或configuration檔案中如 InitMongoService 這樣的各種初始化函數。這時,像 GetApp 這樣的函數就可以使用這些服務和串連了。當然,有時候我們會忘記初始化全域變數,被 nil 引發panic。 雖然在建立全域變數的時候共用資源讓它們(至少)有兩個缺點: 首先,因為組件的依賴關係不明確,所以代碼是很難寫的; 其次,你很難去測試你寫的代碼,在並行條件下更是幾乎不可能。 儘管測試是非常快的(我們希望確保一直很快),但是能夠在並行環境下測試才是最重要的。使用全域連線物件時,後台服務無法在並發條件下測試出相同的資料。 |
晏雨涵 翻譯於 1 個月 前 0人頂 頂 翻譯的不錯哦! |
清除全域變數 為了清除全域變數,我們先從一個通用模式開始。我們的組件現在顯示依賴,我們將,一個Mongo服務,或者一個快取服務。大致來講,我們上面那個幼稚的例子現在看起來應當是這樣的: type AppLoader struct { MongoService mongo.Service} func (l *AppLoader) Get(id uint64) *App { a := new(App) l.MongoService.Session().Find(..).One(a) return a} 許多引用全域變數的函數現在變成了結構體中儲存了它們的依賴。 |
0x0bject 翻譯於 2 個月 前 0人頂 頂 翻譯的不錯哦! |
新的問題 真棒!在main()方法中,我們用一系列的構造代替了全域變數和函數,解決了我們之前遇到的問題。但是... 一看main()函數就知道了,太雜亂無章了。 一開始就這麼亂了: func main() { mongoURL := flag.String(...) mongoService := mongo.NewService(mongoURL) cacheService := cache.NewService(...) appLoader := &AppLoader{ MongoService: mongoService, } handlerOne := &HandlerOne{ AppLoader: appLoader, } handlerTwo := &HandlerTwo{ AppLoader: appLoader, CacheService: cacheService, } rootHandler := &RootHandler{ HandlerOne: handlerOne, HandlerTwo: handlerTwo, } ...} 如果一直這樣寫下去,main()函數的方法體將會被被大量的代碼佔據。而這些代碼僅僅只是做了兩件很普通的事情:分配記憶體空間、裝配對象和組件關係。如果我們有非常多的二進位代碼和庫需要引用,我們就需要一遍又一遍的寫這些無聊的代碼。這裡特別需要注意的是,不要被nil引發panic。比如我們忘記把CacheService傳遞給HandlerTwo,然後就引發了一個運行時panic。我們試圖構造一個方法,但是卻變得有些失控。還需要寫一大堆的代碼手動檢查nil。因為必須手動裝配對象並確保運行正常,我們的開發對此非常惱火。測試人員甚至還需要自己裝配對象、構建關係,顯然他們不會在main()函數中共用這些代碼。所以測試代碼也變得越來越繁雜、冗餘,卻還是經常找不出實際問題。簡而言之,我們解決了一個問題,卻產生了另一種問題。 |
晏雨涵 翻譯於 1 個月 前 0人頂 頂 翻譯的不錯哦! |
標識 Mundane
我們中的一些人對DI系統比較有經驗,並且我們都不認為這僅僅是純娛樂性的經驗。因此,當我們第一次討論用 DI系統解決這個新問題時,就已經有大量的push back(我理解為經驗儲備...高手求解)。 根據這些規則,當我們需要一些東西的時候,我們決定需要確保避免已知的複雜性並制定了一些基本準則: 1. 沒有代碼產生。我們的開發編譯步驟僅僅用 go install,我們不想引入額外的步驟。與這條規則相關的是無檔案掃描,我們不想把項目變成一個O(大量檔案)系統,同時也要防止增加編譯時間。 2. 沒有子圖。子圖的概念是以每個請求為基準(a per-request basis)允許注入發生,簡單來說,一個子圖必須能夠徹底地區分"global"生命週期和"per-request"(每個請求)生命週期的對象,並且確保在所有請求中不混淆這些"per-request"對象。我們決定僅僅允許"global"生命週期對象的注入,因為這正是我們現在面臨的問題。 3. 避免代碼執行。DI本質上使代碼很難理解,我們想避免定製化的代碼執行/鉤子,使它更容易理解。 根據這些準則,我們的目標變得比較清晰了: 1. 注入應該指派至。 2. 注入應該將對象圖串連起來。 3. 注入應該在程式啟動時僅僅運行一次。 我們也討論了supporting constructor(支援建構函式)功能,但現在避免對他們增加支援。 |
xiaoaiwhc1 翻譯於 1 個月 前 0人頂 頂 翻譯的不錯哦! |
注入庫是這項工作的成果和我們的解決方案。它使用結構標籤(struct tags)來實現注入功能,可為具體的類型注入,也支援對介面類型注入,只要明確介面類型的具體類型,它還有些不太常用的功能,比如按名稱注入(named injection)。前面的簡單樣本現在看起來是這樣: type AppLoader struct { MongoService mongo.Service `inject:""`} func (l *AppLoader) Get(id uint64) *App { a := new(App) l.MongoService.Session().Find(..).One(a) return a} 沒有任何改變,除了在 MongoService 欄位上增加了注入標籤。有幾種不同的方式使用注入標籤,但這是最常見用法,它簡潔地表明了期望注入一個 mongo.Service 執行個體。同樣地,可以想象 HandlerOne,HandlerTwo 和 RootHandler 欄位上也有注入標籤。 |
張露兵 翻譯於 2 個月 前 1人頂 頂 翻譯的不錯哦! |
我們的main()現在看起來這樣: func main() { mongoURL := flag.String(...) mongoService := mongo.NewService(mongoURL) cacheService := cache.NewService(...) var app RootHandler err := inject.Populate(mongoService, cacheService, &app) if err != nil { panic(err) } ...} 更短!注入的整個流程大概是這樣: 1. 查看每個已經提供的執行個體,最終遇到RootHandler類型的app執行個體. 2. 查看RootHandler欄位,尋找帶 inject 標籤的*HandlerOne,發現沒有*HandlerOne執行個體存在,於是就建立一個並將它賦值給這個欄位. 3. 對剛剛建立的HandlerOne執行個體繼續進行與步驟2類似的尋找,找到AppLoader欄位,簡單地建立它. 4. 對於AppLoader執行個體,它需要一個mongo.Service執行個體,它發現當我們調用Populate時已經建立過一個執行個體,於是它將那個執行個體賦值到這裡. 5. 當它對HandlerTwo進行同樣的尋找時,它使用已經建立的AppLoader執行個體,因此這兩個Handlers共用這個AppLoader執行個體. 注入指派至並為我們將graph串連起來。調用Populate後,注入不再做任何事情,剩下的跟之前沒有注入時的行為都一樣了. |
xiaoaiwhc1 翻譯於 1 個月 前 0人頂 頂 翻譯的不錯哦! |
勝利啦 我們的main()函數更易管控了。現在,手動建立一個僅有兩個case的執行個體:如果執行個體需要在main中得到配置資訊,或者如果其需要請求一個介面類型。即使如此,我們往往建立一些不完整的執行個體,讓依賴注入為我們補充完整。測試代碼大幅度的精簡,並且現在可以在不需要知道對象圖表的情況下為測試提供執行。這使得測試更具彈性,可以改變相當大。重構同樣變得簡單起來,就像抽出邏輯而不需要手動調整我們在各類main()中建立的對象圖表。 總體來說,我們對結果和自從介紹了依賴注入,我們的程式碼程式庫的演化感到非常高興。 資源 你可以在Github上找到該庫的資源: https://github.com/facebookgo/inject 我們同時提供文檔,儘管最好的學習方式是實際“玩”一下: https://godoc.org/github.com/facebookgo/inject 我們非常喜愛能得到貢獻,所以在貢獻時請確保下面的測試可以通過: https://travis-ci.org/facebookgo/inject |
0x0bject 翻譯於 2 個月 前 0人頂 頂 翻譯的不錯哦! |
本文中的所有譯文僅用於學習和交流目的,轉載請務必註明文章譯者、出處、和本文連結
我們的翻譯工作遵照 CC 協議,如果我們的工作有侵犯到您的權益,請及時聯絡我們