Resolver介紹

本文為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。

了解有關(guān)注冊的詳細(xì)信息

解析

現(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í)例,解析器就遵從了。

了解有關(guān)解析解析周期的更多信息。

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)在您可以輕松地測試不同的場景。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容