Swift 中風(fēng)味各異的依賴注入

在之前的文章中抹恳,我們看了一些使用依賴注入的不同方法犬辰,以實(shí)現(xiàn)Swift應(yīng)用中更多的解耦和可測(cè)試架構(gòu)钞翔。例如黑忱, "在Swift中使用工廠的依賴注入"中把依賴注入和工廠模式結(jié)合起來,以及"在Swift中避免使用單利" 中利用依賴注入取代單利袄膏。

到目前為止,我的大部分文章和例子都使用了基于初始化器的依賴注入掺冠。然而沉馆,就像大多數(shù)編程技術(shù)一樣,依賴注入有多種“風(fēng)味(Flavors)”德崭,每一種都有自己的優(yōu)點(diǎn)和缺點(diǎn)斥黑。本周,讓我們來看看三種不同方式的依賴注入眉厨,以及它們?nèi)绾卧赟wift中使用锌奴。

基于初始化器

讓我們先快速回顧一下最常見的依賴注入方式——基于初始化器的依賴注入,即對(duì)象在被初始化時(shí)應(yīng)該被賦予它所需要的依賴關(guān)系憾股。這種方式的最大好處是鹿蜀,它保證我們的對(duì)象擁有它們所需要的一切箕慧,以便立即開展工作。

假設(shè)我們正在構(gòu)建一個(gè)從磁盤上加載文件的FileLoader茴恰。為了做到這一點(diǎn)颠焦,它使用了兩個(gè)依賴項(xiàng)——一個(gè)是系統(tǒng)提供的FileManager的實(shí)例,另一個(gè)是Cache往枣。使用基于初始化器的依賴注入伐庭,可以這樣實(shí)現(xiàn):

class FileLoader {
    private let fileManager: FileManager
    private let cache: Cache

    init(fileManager: FileManager = .default,
         cache: Cache = .init()) {
        self.fileManager = fileManager
        self.cache = cache
    }
}

注意上面是如何使用默認(rèn)參數(shù)的,以避免在使用單例或新實(shí)例時(shí)總是創(chuàng)建依賴關(guān)系分冈。這使我們能夠在生產(chǎn)代碼中使用FileLoader()簡(jiǎn)單地創(chuàng)建一個(gè)文件加載器圾另,同時(shí)仍然能夠通過在測(cè)試代碼中注入模擬數(shù)據(jù)或顯式實(shí)例進(jìn)行測(cè)試。

基于屬性

雖然基于初始化器的依賴注入通常很適合你自己的自定義類雕沉,但有時(shí)當(dāng)你必須從系統(tǒng)類繼承時(shí)集乔,它就有點(diǎn)難用了。一個(gè)例子是在構(gòu)建視圖控制器時(shí)蘑秽,特別是當(dāng)你使用 XIBs 或 Storyboards 來定義它們時(shí)饺著,因?yàn)檫@樣你就無法再控制你的類的初始化器了。

對(duì)于這些類型的情況肠牲,基于屬性的依賴注入可以是一個(gè)很好的選擇幼衰。與其在對(duì)象的初始化器中注入對(duì)象的依賴關(guān)系,不如在之后簡(jiǎn)單地將其分配缀雳。這種依賴注入的方式也可以幫助你減少模板文件渡嚣,特別是當(dāng)有一個(gè)好的默認(rèn)值不一定需要注入的時(shí)候。

讓我們來看看另一個(gè)例子——在這個(gè)例子中肥印,我們要建立一個(gè)PhotoEditorViewController识椰,讓用戶編輯他們庫中的一張照片。為了發(fā)揮作用深碱,這個(gè)視圖控制器需要一個(gè)系統(tǒng)提供的PHPhotoLibrary類的實(shí)例(它是一個(gè)單例)腹鹉,以及一個(gè)我們自己的PhotoEditorEngine類的實(shí)例。為了在沒有自定義初始化器的情況下實(shí)現(xiàn)依賴性注入敷硅,我們可以創(chuàng)建兩個(gè)都有默認(rèn)值的可變屬性功咒,就像這樣:

class PhotoEditorViewController: UIViewController {
    var library: PhotoLibrary = PHPhotoLibrary.shared()
    var engine = PhotoEditorEngine()
}

請(qǐng)注意 "通過 3 個(gè)簡(jiǎn)單的步驟測(cè)試使用了系統(tǒng)單例的 Swift 代碼"中的技術(shù)是如何通過使用協(xié)議來為系統(tǒng)照片庫類提供一個(gè)更抽象的PhotoLibrary接口。這將使測(cè)試和數(shù)據(jù)模擬變得更加容易!

上述做法的好處是绞蹦,我們?nèi)匀豢梢院苋菀椎卦跍y(cè)試中注入模擬數(shù)據(jù)力奋,只需重新分配視圖控制器的屬性:

class PhotoEditorViewControllerTests: XCTestCase {
    func testApplyingBlackAndWhiteFilter() {
        let viewController = PhotoEditorViewController()

        // 分配一個(gè)模擬照片庫以完全控制里面存儲(chǔ)了哪些照片
        let library = PhotoLibraryMock()
        library.photos = [TestPhotoFactory.photoWithColor(.red)]
        viewController.library = library

        // 運(yùn)行我們的測(cè)試命令
        viewController.selectPhoto(atIndex: 0)
        viewController.apply(filter: .blackAndWhite)
        viewController.savePhoto()

        // 斷言結(jié)果是正確的
        XCTAssertTrue(photoIsBlackAndWhite(library.photos[0]))
    }
}

基于參數(shù)

最后,讓我們看一下基于參數(shù)的依賴注入幽七。當(dāng)你想輕松地使遺留代碼變得更容易測(cè)試且不必過多地改變其現(xiàn)有結(jié)構(gòu)時(shí)景殷,這種類型特別有用。

很多時(shí)候猿挚,我們只需要一個(gè)特定的依賴關(guān)系一次咐旧,或者我們只需要在某些條件下模擬它。我們不需要改變對(duì)象的初始化器或?qū)傩员┞稙榭勺兊模ㄟ@并不總是一個(gè)好方式)亭饵,而是可以開放某個(gè)API來接受一個(gè)依賴關(guān)系作為參數(shù)。

讓我們來看看一個(gè)NoteManager類辜羊,它是一個(gè)記事應(yīng)用程序的一部分。它的工作是管理用戶所寫的所有筆記碱妆,并提供一個(gè)API用于根據(jù)查詢來搜索筆記。由于這是一個(gè)可能需要一段時(shí)間的操作(如果用戶有很多筆記的話昔驱,這是很有可能的)骤肛,我們通常在一個(gè)后臺(tái)隊(duì)列中執(zhí)行纳本,像這樣:

class NoteManager {
    func loadNotes(matching query: String,
                   completionHandler: @escaping ([Note]) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            let database = self.loadDatabase()
            let notes = database.filter { note in
                return note.matches(query: query)
            }

            completionHandler(notes)
        }
    }
}

雖然上述方法對(duì)我們的生產(chǎn)代碼來說是一個(gè)很好的解決方案,但在測(cè)試中腋颠,我們通常希望盡可能地避免異步代碼和并行性繁成,以避免片狀現(xiàn)象。雖然使用初始化器或基于屬性的依賴注入來指定NoteManager應(yīng)始終使用的顯式隊(duì)列會(huì)很好淑玫,但這可能需要對(duì)類進(jìn)行大的修改巾腕,而我們現(xiàn)在還不能/不愿意這樣做。

這就是基于參數(shù)的依賴性注入的作用絮蒿。與其重構(gòu)我們的整個(gè)類尊搬,不如直接注入要在哪個(gè)隊(duì)列上運(yùn)行loadNotes操作:

class NoteManager {
    func loadNotes(matching query: String,
                   on queue: DispatchQueue = .global(qos: .userInitiated),
                   completionHandler: @escaping ([Note]) -> Void) {
        queue.async {
            let database = self.loadDatabase()
            let notes = database.filter { note in
                return note.matches(query: query)
            }

            completionHandler(notes)
        }
    }
}

這使我們能夠在測(cè)試代碼中輕松地使用一個(gè)自定義隊(duì)列,我們可以在上面等待土涝。這幾乎可以讓我們?cè)跍y(cè)試中把上述API變成一個(gè)同步的API佛寿,這讓事情變得更容易和更可預(yù)測(cè)。

基于參數(shù)的依賴注入的另一個(gè)用例是當(dāng)你想測(cè)試靜態(tài)API的時(shí)候但壮。對(duì)于靜態(tài)API冀泻,我們沒有初始化器,而且我們最好也不要靜態(tài)地保持任何狀態(tài)茵肃,所以基于參數(shù)的依賴注入成為一個(gè)很好的選擇。讓我們看一個(gè)當(dāng)前依賴單例的靜態(tài)MessageSender 類:

class MessageSender {
    static func send(_ message: Message, to user: User) throws {
        Database.shared.insert(message)

        let data: Data = try wrap(message)
        let endpoint = Endpoint.sendMessage(to: user)
        NetworkManager.shared.post(data, to: endpoint.url)
    }
}

雖然理想的長(zhǎng)期解決方案可能是重構(gòu)MessageSender袭祟,使其成為非靜態(tài)的验残,并在其使用的任何地方正確注入,但為了方便測(cè)試(例如巾乳,為了重現(xiàn)/驗(yàn)證一個(gè)錯(cuò)誤)您没,我們可以簡(jiǎn)單地將其依賴性作為參數(shù)注入鸟召,而不是依賴單例:

class MessageSender {
    static func send(_ message: Message,
                     to user: User,
                     database: Database = .shared,
                     networkManager: NetworkManager = .shared) throws {
        database.insert(message)

        let data: Data = try wrap(message)
        let endpoint = Endpoint.sendMessage(to: user)
        networkManager.post(data, to: endpoint.url)
    }
}

我們?cè)俅问褂媚J(rèn)參數(shù),除去為了方便的原因氨鹏,但這里更重要的是為了能夠在我們的代碼中添加測(cè)試支持欧募,同時(shí)仍然保持100%的向后兼容性。

\color{orange}{\Large \mathtt{Swift 中風(fēng)味各異的依賴注入}}

譯自 John Sundell 的 Different flavors of dependency injection in Swift

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末仆抵,一起剝皮案震驚了整個(gè)濱河市跟继,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镣丑,老刑警劉巖舔糖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異莺匠,居然都是意外死亡金吗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門趣竣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摇庙,“玉大人,你說我怎么就攤上這事遥缕∥捞唬” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵通砍,是天一觀的道長(zhǎng)玛臂。 經(jīng)常有香客問我,道長(zhǎng)封孙,這世上最難降的妖魔是什么迹冤? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮虎忌,結(jié)果婚禮上泡徙,老公的妹妹穿的比我還像新娘。我一直安慰自己膜蠢,他們只是感情好堪藐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著挑围,像睡著了一般礁竞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杉辙,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天模捂,我揣著相機(jī)與錄音,去河邊找鬼。 笑死狂男,一個(gè)胖子當(dāng)著我的面吹牛综看,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播岖食,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼红碑,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了泡垃?” 一聲冷哼從身側(cè)響起析珊,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兔毙,沒想到半個(gè)月后唾琼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡澎剥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年锡溯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哑姚。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡祭饭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叙量,到底是詐尸還是另有隱情倡蝙,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布绞佩,位于F島的核電站寺鸥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏品山。R本人自食惡果不足惜胆建,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肘交。 院中可真熱鬧笆载,春花似錦、人聲如沸涯呻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽复罐。三九已至涝登,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間效诅,已是汗流浹背胀滚。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工咳短, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛛淋。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像篡腌,于是被迫代替她去往敵國(guó)和親褐荷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容