在 Swift 中使用工廠進(jìn)行依賴注入

當(dāng)涉及到使代碼更加可測試時雪位,依賴注入是一個重要工具。與其讓對象創(chuàng)建自己的依賴關(guān)系或作為單例訪問它們忽你,不如讓對象在工作中需要的一切都從外部傳入蔗牡。這使我們更容易看到一個給定的對象有哪些確切的依賴關(guān)系,同時也使測試變得更加簡單——因?yàn)榭梢阅M依賴項(xiàng)以捕獲和驗(yàn)證狀態(tài)和值愚臀。

然而忆蚀,盡管它很有用,但如果在一個項(xiàng)目中廣泛使用姑裂,依賴注入也會成為一個相當(dāng)大的痛點(diǎn)馋袜。隨著一個給定對象的依賴數(shù)量的增加,初始化它可能成為一個相當(dāng)麻煩的事情炭分。讓代碼可測試是件好事桃焕,但如果要以這樣的初始化器為代價,那就太糟糕了:

class UserManager {
    init(dataLoader: DataLoader, database: Database, cache: Cache,
         keychain: Keychain, tokenManager: TokenManager) {
        ...
    }
}

本周捧毛,讓我們來看看一種依賴注入技術(shù)观堂,它可以讓我們實(shí)現(xiàn)可測試性让网,而不強(qiáng)迫我們寫這種大規(guī)模的初始化器或復(fù)雜的依賴管理代碼。

傳遞依賴關(guān)系

在使用依賴注入時师痕,我們經(jīng)常會出現(xiàn)上述情況溃睹,主要原因是我們需要傳遞依賴關(guān)系,以便以后使用它們胰坟。例如因篇,假設(shè)我們正在構(gòu)建一個消息應(yīng)用程序,我們有一個視圖控制器來顯示用戶的所有消息:

class MessageListViewController: UITableViewController {
    private let loader: MessageLoader

    init(loader: MessageLoader) {
        self.loader = loader
        super.init(nibName: nil, bundle: nil)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        loader.load { [weak self] messages in
            self?.reloadTableView(with: messages)
        }
    }
}

正如你所看到的笔横,我們將一個MessageLoader注入到MessageListViewController中竞滓,然后用它來加載數(shù)據(jù)。這還不算太糟吹缔,因?yàn)槲覀冎挥幸粋€依賴關(guān)系商佑。然而,我們的列表視圖很可能不是只有一層厢塘,這在某種程度上需要我們實(shí)現(xiàn)導(dǎo)航到另一個視圖控制器茶没。

假設(shè)我們想讓用戶在點(diǎn)擊消息列表中的某個單元格時,能夠?qū)Ш降揭粋€新的視圖晚碾。對于這個新的視圖抓半,我們創(chuàng)建了一個MessageViewController,它既可以讓用戶查看消息的全文格嘁,也可以對其進(jìn)行回復(fù)笛求。為了啟用回復(fù)功能,我們實(shí)現(xiàn)了一個MessageSender類讥蔽,在創(chuàng)建新的視圖控制器時涣易,我們將其注入到新的視圖控制器中,像這樣:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let message = messages[indexPath.row]
    let viewController = MessageViewController(message: message, sender: sender)
    navigationController?.pushViewController(viewController, animated: true)
}

問題來了冶伞。由于MessageViewController需要一個MessageSender的實(shí)例新症,我們也需要讓MessageListViewController知道這個類。一個選擇是簡單地將發(fā)送者也添加到列表視圖控制器的初始化器中:

class MessageListViewController: UITableViewController {
    init(loader: MessageLoader, sender: MessageSender) {
        ...
    }
}

雖然上面的方法可行响禽,但它開始把我們引向另一個龐大的初始化器徒爹,并使MessageListViewController變得更難使用(也相當(dāng)令人困惑,為什么列表首先需要知道發(fā)件人芋类???).

另一個可能的解決方案(在這種情況下很常見)是讓MessageSender成為一個單例隆嗅。這樣我們就可以很容易地從任何地方訪問它,并通過簡單地使用它的共享實(shí)例將其注入MessageViewController中:

let viewController = MessageViewController(
    message: message,
    sender: MessageSender.shared
)

然而侯繁,就像我們在 "避免在Swift中使用單例 "中看到的那樣胖喳,單例方法也有一些明顯的缺點(diǎn),可能會導(dǎo)致我們陷入一種難以理解的架構(gòu)和不明確的依賴關(guān)系的局面贮竟。

工廠模式來救援

如果我們能跳過上述所有的步驟丽焊,讓MessageListViewController完全不知道MessageSender较剃,以及其他任何后續(xù)視圖控制器可能需要的依賴關(guān)系,那不是更好嗎技健?

如果我們能有某種形式的工廠写穴,我們可以簡單地要求它為給定的消息創(chuàng)建一個MessageViewController,這將是非常方便的(甚至比引入一個單例更方便)雌贱,而且非常干凈啊送,像這樣:

let viewController = factory.makeMessageViewController(for: message)

就像我們在 "使用工廠模式來避免Swift中的共享狀態(tài) "中看到的那樣,我非常喜歡工廠的一點(diǎn)是欣孤,它可以讓你完全解耦對象的使用和創(chuàng)建馋没。這使得許多對象與它們的依賴關(guān)系更加松散,這在你想要重構(gòu)或改變事物的情況下非常有幫助降传。

那么披泪,我們?nèi)绾尾拍苁股鲜銮闆r發(fā)生呢?

我們將首先為我們的工廠定義一個協(xié)議搬瑰,這將使我們能夠輕松地創(chuàng)建我們應(yīng)用程序中需要的任何視圖控制器,而不需要實(shí)際了解其依賴性或初始化器控硼。

protocol ViewControllerFactory {
    func makeMessageListViewController() -> MessageListViewController
    func makeMessageViewController(for message: Message) -> MessageViewController
}

但我們不會止步于此泽论。我們還將創(chuàng)建額外的工廠協(xié)議來創(chuàng)建我們的視圖控制器的依賴關(guān)系,比如這個卡乾,讓我們?yōu)槲覀兊牧斜硪晥D控制器創(chuàng)建一個MessageLoader:

protocol MessageLoaderFactory {
    func makeMessageLoader() -> MessageLoader
}

單一的依賴性

一旦我們建立了工廠協(xié)議翼悴,我們就可以回到MessageListViewController,并重構(gòu)它幔妨,使其不再接受其依賴的實(shí)例——它現(xiàn)在只接受一個工廠鹦赎。

class MessageListViewController: UITableViewController {
    // 這里我們使用協(xié)議組合來創(chuàng)建一個工廠類型,
    // 其中包括這個視圖控制器需要的所有工廠協(xié)議误堡。
    typealias Factory = MessageLoaderFactory & ViewControllerFactory

    private let factory: Factory
     // 我們現(xiàn)在可以使用注入的工廠懶加載我們的 MessageLoader古话。
    private lazy var loader = factory.makeMessageLoader()

    init(factory: Factory) {
        self.factory = factory
        super.init(nibName: nil, bundle: nil)
    }
}

通過上述操作,我們現(xiàn)在已經(jīng)完成了兩件事锁施。首先陪踩,我們將我們的依賴列表縮減為一個工廠,而且我們不再需要讓MessageListViewController知道MessageViewController的依賴關(guān)系悉抵。

創(chuàng)建容器

現(xiàn)在是時候?qū)崿F(xiàn)我們的工廠協(xié)議了肩狂。要做到這一點(diǎn),我們首先要定義一個DependencyContainer姥饰,它將包含我們應(yīng)用程序的所有核心實(shí)用對象傻谁,這些對象通常作為依賴關(guān)系被直接注入。這包括像之前的MessageSender列粪,但也包括更多的低級邏輯類审磁,比如我們可能使用的NetworkManager谈飒。

class DependencyContainer {
    private lazy var messageSender = MessageSender(networkManager: networkManager)
    private lazy var networkManager = NetworkManager(urlSession: .shared)
}

正如你在上面看到的,我們使用了lazy屬性力图,以便在初始化我們的對象時能夠引用同一類別的其他屬性步绸。這是一個非常方便和漂亮的設(shè)置依賴關(guān)系的方法,因?yàn)槟憧梢岳镁幾g器來幫助你避免循環(huán)依賴等問題吃媒。

最后瓤介,我們將使我們的新依賴容器遵守我們的工廠協(xié)議,這將使我們能夠把它作為工廠注入到我們的各種視圖控制器和其他對象赘那。

extension DependencyContainer: ViewControllerFactory {
    func makeMessageListViewController() -> MessageListViewController {
        return MessageListViewController(factory: self)
    }

    func makeMessageViewController(for message: Message) -> MessageViewController {
        return MessageViewController(message: message, sender: messageSender)
    }
}

extension DependencyContainer: MessageLoaderFactory {
    func makeMessageLoader() -> MessageLoader {
        return MessageLoader(networkManager: networkManager)
    }
}

分散所有權(quán)

現(xiàn)在是拼圖的最后一塊——我們究竟在哪里存儲我們的依賴容器刑桑,誰應(yīng)該擁有它,它應(yīng)該被設(shè)置在哪里募舟?最酷的是:因?yàn)槲覀儗⒆⑷胛覀兊囊蕾囆匀萜髯鳛槲覀兊膶ο笏璧墓S的實(shí)現(xiàn)祠斧,而且這些對象將持有對其工廠的強(qiáng)引用——我們沒有必要將容器存儲在其他地方。

例如拱礁,如果MessageListViewController是我們應(yīng)用程序的初始視圖控制器琢锋,我們可以簡單地創(chuàng)建一個DependencyContainer的實(shí)例并將其傳入:

let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()

window.rootViewController = UINavigationController(
    rootViewController: listViewController
)

不需要在任何地方保留任何全局變量,也不需要在應(yīng)用程序委托中使用可選屬性呢灶。

小結(jié)

使用工廠協(xié)議和容器來設(shè)置你的依賴注入是一個很好的方法吴超,可以避免傳遞多個依賴關(guān)系,以及不得不創(chuàng)建復(fù)雜的初始化器鸯乃。雖然這不是銀彈鲸阻,但它可以使依賴注入的使用更容易——這將使你更清楚地了解你的對象的實(shí)際依賴關(guān)系,同時也使測試更簡單缨睡。

由于我們已經(jīng)將所有的工廠定義為協(xié)議鸟悴,我們可以通過實(shí)現(xiàn)任何給定工廠協(xié)議的特定測試版本,在測試中輕松地模擬它們奖年。我將在未來的博文中寫更多關(guān)于模擬和如何在測試中充分利用依賴注入的內(nèi)容细诸。

你怎么看?你以前使用過像這樣的解決方案嗎陋守,或者你會嘗試一下嗎揍堰?

感謝您的閱讀 !

\color{orange}{\Large \mathtt{在 Swift 中使用工廠進(jìn)行依賴注入}}

譯自 John SundellDependency injection using factories in Swift

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嗅义,隨后出現(xiàn)的幾起案子屏歹,更是在濱河造成了極大的恐慌,老刑警劉巖之碗,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝙眶,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)幽纷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門式塌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人友浸,你說我怎么就攤上這事峰尝。” “怎么了收恢?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵武学,是天一觀的道長。 經(jīng)常有香客問我伦意,道長火窒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任驮肉,我火速辦了婚禮熏矿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘离钝。我一直安慰自己票编,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布卵渴。 她就那樣靜靜地躺著栏妖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奖恰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天宛裕,我揣著相機(jī)與錄音瑟啃,去河邊找鬼。 笑死揩尸,一個胖子當(dāng)著我的面吹牛蛹屿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播岩榆,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼错负,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了勇边?” 一聲冷哼從身側(cè)響起犹撒,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎粒褒,沒想到半個月后识颊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奕坟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年祥款,在試婚紗的時候發(fā)現(xiàn)自己被綠了清笨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡刃跛,死狀恐怖抠艾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情桨昙,我是刑警寧澤检号,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站绊率,受9級特大地震影響谨敛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滤否,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一脸狸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧藐俺,春花似錦炊甲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至菱父,卻和暖如春颈娜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背浙宜。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工官辽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人粟瞬。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓同仆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親裙品。 傳聞我的和親對象是個殘疾皇子俗批,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

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