本文為Resolver-Swift版超輕依賴注入/服務(wù)定位器框架中的鏈接。
也是GitHub 中 Resolver: Introduction的翻譯。
定義
Resolver是Swift依賴注入框架,支持控制設(shè)計(jì)反轉(zhuǎn)模式(Inversion of Control design pattern)。
除了計(jì)算機(jī)科學(xué)定義,依賴注入基本上歸結(jié)為:
| 給一個對象提供它需要做的事情。
依賴注入允許我們編寫松散耦合的代碼,因此,更易于重用、模擬和測試。
簡單的例子
這里有一個需要與NetworkService通信的對象。
class MyViewModel {
let service = NetworkService()
func load() {
let data = service.getData()
}
}
該類被認(rèn)為與它的依賴項(xiàng)NetworkService緊密耦合。
問題是MyObject總是這樣創(chuàng)建它自己的NetworkService類型的服務(wù)。
但是如果我們希望MyViewModel從磁盤中提取數(shù)據(jù)呢?如果我們想在代碼中的其他地方重用MyViewModel,或者在另一個應(yīng)用程序中,讓它提取不同的數(shù)據(jù)呢?
如果我們想對MyViewModel的測試結(jié)果進(jìn)行模擬要怎么做呢?
或者為了達(dá)到質(zhì)量保證的目的,要讓應(yīng)用程序完全運(yùn)行在mock數(shù)據(jù)上要怎么做呢?
注入
現(xiàn)在,考慮一個依賴于傳遞給它的NetworkService實(shí)例的對象,使用我們的DI(Dependency Injection依賴注入)類型的術(shù)語進(jìn)行屬性注入。
class MyViewModel {
var service: NetworkServicing!
func load() {
let data = service.getData()
}
}
MyViewModel現(xiàn)在依賴于預(yù)先設(shè)置的網(wǎng)絡(luò)服務(wù),而不是直接實(shí)例化NetworkService本身的副本。
此外,MyViewModel現(xiàn)在使用一個名為NetworkServicing的協(xié)議,它反過來定義了一個getData()方法。
這兩個變化使我們能夠?qū)崿F(xiàn)上述所有目標(biāo)。
將NetworkServicing的正確實(shí)現(xiàn)傳遞給MyViewModel,然后可以從網(wǎng)絡(luò)、緩存、磁盤上的測試文件或模擬數(shù)據(jù)池中提取數(shù)據(jù)。
很好。但是這種方法只是做到了把罐子踢得更遠(yuǎn)嗎?
如何獲取MyViewModel以及MyViewModel如何獲得NetworkService的正確版本?我不需要自己創(chuàng)建它并設(shè)置它的屬性嗎?
好吧,你可以,但是更好的答案是使用依賴注入。
注冊
依賴注入分為兩個階段:注冊 和 解析。
注冊包括注冊我們需要的類和對象,以及在需要時提供一個工廠閉包來創(chuàng)建一個實(shí)例。
Resolver.register { NetworkService() as NetworkServicing }
Resolver.register { MyViewModel() }.resolveProperties { (_, model) in
model.service = optional() // note NetworkServicing was defined as an ImplicitlyUnwrappedOptional
}
上面的內(nèi)容看起來有點(diǎn)復(fù)雜,但實(shí)際上相當(dāng)簡單。
首先,我們注冊了一個工廠(閉包),它將在需要時創(chuàng)建NetworkService的實(shí)例。使用工廠返回的結(jié)果類型自動推斷正在注冊的類型。
因此,我們創(chuàng)建了一個NetworkService,但是我們實(shí)際注冊了協(xié)議NetworkService。
類似地,我們注冊了一個工廠,以便在需要時創(chuàng)建MyViewModel,還添加了resolveProperties閉包來解析其服務(wù)屬性。
解析
一旦注冊,任何對象都可以要求Resolver提供(解析)該類型的對象。
var viewModel: MyViewModel = Resolver.resolve()
為什么這么麻煩?
所以我們注冊了一個工廠,并要求Resolver解析它和它的工作..但是為什么要制造這些額外的麻煩呢?
為什么我們不直接實(shí)例化MyViewModel并完成它呢?
var viewModel = MyViewModel()
viewModel.service = NetworkService()
好吧,這是導(dǎo)致這個‘壞主意’的產(chǎn)生的幾個原因,但讓我們從兩個方面開始:
首先,如果NetworkService反過來需要其他類或?qū)ο髞硗瓿伤墓ぷ?,會發(fā)生什么情況?如果這些對象需要引用其他對象、服務(wù)和系統(tǒng)資源,會發(fā)生什么?
var viewModel = MyViewModel()
viewModel.service = NetworkService(TokenVendor.token(AppDelegate.seed))
你只需要構(gòu)造所需的對象...構(gòu)建所需的對象...來構(gòu)建您最初真正想要的對象的單個實(shí)例。
這些附加對象稱為依賴項(xiàng)。
其次,更糟的是,構(gòu)造類現(xiàn)在知道MyViewModel和NetworkService的內(nèi)部和需求,還知道TokenVendor及其需求。
當(dāng)它真正想做的只是和一個MyViewModel交談時,現(xiàn)在卻與所有這些類的行為和不同的實(shí)現(xiàn)緊密耦合...
ViewController、ViewModel和服務(wù)。哦,天的天哪。
為了演示,讓我們用一個更復(fù)雜的例子。
這里有一個名為MyViewController的UIViewController,它需要一個XYZViewModel的實(shí)例。
class MyViewController: UIViewController {
var viewModel: XYZViewModel!
}
XYZViewModel需要一個實(shí)現(xiàn)XYZFetching協(xié)議的對象實(shí)例,一個實(shí)現(xiàn)XYZUpdating的對象實(shí)例,它還需要訪問XYZService。
反過來,XYZService需要對XYZSessionService的引用來完成其工作。
class XYZViewModel {
private var fetcher: XYZFetching
private var updater: XYZUpdating
private var service: XYZService
init(fetcher: XYZFetching, updater: XYZUpdating, service: XYZService) {
self.fetcher = fetcher
self.updater = updater
self.service = service
}
// Implmentation
}
class XYZCombinedService: XYZFetching, XYZUpdating {
private var session: XYZSessionService
init(_ session: XYZSessionService) {
self.session = session
}
// Implmentation
}
struct XYZService {
// Implmentation
}
class XYZSessionService {
// Implmentation
}
請注意,XYZViewModel和XYZCombinedService的初始值設(shè)定項(xiàng)都傳遞了它們執(zhí)行任務(wù)所需的對象。使用依賴注入行話,這被稱為初始化或構(gòu)造函數(shù)注入,這是對象構(gòu)造的推薦方法。
注冊
讓我們使用Resolver來注冊這些類。
這里我們用ResolverRegistering協(xié)議擴(kuò)展了基本解析器類,它幾乎只告訴Resolver我們已經(jīng)添加了registerAllServices()函數(shù)。
registerAllServices函數(shù)在Resolver第一次被要求解析服務(wù)時自動調(diào)用,實(shí)際上是執(zhí)行解析系統(tǒng)的一次性初始化。
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
register { XYZViewModel(fetcher: resolve(), updater: resolve(), service: resolve()) }
register { XYZCombinedService(resolve()) }
.implements(XYZFetching.self)
.implements(XYZUpdating.self)
register { XYZService() }
register { XYZSessionService() }
}
}
因此,上面的代碼向我們展示了如何注冊XYZViewModel、協(xié)議XYZFetching和xyzupdate、XYZCombinedService、xyz服務(wù)和XYZSessionService。
解析
現(xiàn)在我們已經(jīng)注冊了應(yīng)用程序?qū)⒁褂玫乃袑ο蟆5沁@個過程是怎么開始的呢?誰先做出決定的?
好吧,MyViewController是想要一個XYZViewModel的對象,所以讓我們用下面的方法重寫下...
class MyViewController: UIViewController, Resolving {
lazy var viewModel: XYZViewModel = resolver.resolve()
}
采用Resolving protocol將默認(rèn)解析器實(shí)例注入MyViewController(接口注入)。在該實(shí)例上調(diào)用resolve允許它從解析器請求XYZViewModel。
Resolver處理請求,找到正確的工廠來創(chuàng)建XYZViewModel,并告訴它這樣做。
XYZViewModel工廠反過來觸發(fā)它所需類型的解析(XYZFetching、XYZUpdating和XYZService),等等,一直到鏈的下游。最終,XYZViewModel工廠獲得所需的一切,返回正確的實(shí)例,MyViewController獲得其視圖模型。
MyViewController不知道XYZViewModel的內(nèi)部結(jié)構(gòu),也不知道XYZFetcher、XYZUpdater、XYZService或XYZSessionService。
也不需要。它只需向Resolver請求一個類型為T的實(shí)例,解析器就遵從了。
Mocking
好吧,你可能會想。這很酷,但是前面你提到了其他的好處,比如測試和mock。那些呢?
考慮對上述代碼進(jìn)行以下更改:
extension Resolver {
static func registerAllServices() {
register { XYZViewModel(fetcher: resolve(), updater: resolve(), service: resolve()) }
register { XYZCombinedService(resolve()) }
.implements(XYZFetching.self)
.implements(XYZUpdating.self)
register { XYZService() }
register { XYZSessionService() }
#if DEBUG
register { XYZMockSessionService() as XYZSessionService }
#end
}
}
這只是一種方法,但它說明了這個概念?,F(xiàn)在,當(dāng)MyViewController請求XYZViewModel時,它得到了一個。解析的XYZViewModel依次有其fetcher、updater和服務(wù)。
但是,如果我們處于調(diào)試模式,那么fetcher和updater現(xiàn)在有了一個XYZMockSessionService,它可以從嵌入的文件中提取模擬數(shù)據(jù),而不是像平常一樣發(fā)送到服務(wù)器。
而且MyViewController和XYZViewModel都不需要明白這些。
Testing
單元測試也是如此。在單元測試代碼中添加如下內(nèi)容。
let data = ["name":"Mike", "developer":true]
register { XYZTestSessionService(data) as XYZSessionService }
let viewModel: XYZViewModel = Resolver.resolve()
現(xiàn)在,您的單元和集成使用XYZTestSessionService測試XYZViewModel,它向模型提供穩(wěn)定的已知數(shù)據(jù)。
再來一次。
let data = ["name":"Boss", "developer":false]
register { XYZTestSessionService(data) as XYZSessionService }
let viewModel: XYZViewModel = Resolver.resolve()
現(xiàn)在您可以輕松地測試不同的場景。