日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

swift依賴注入和依賴注入容器詳解_Swift

作者:獨孤星岳 ? 更新時間: 2023-03-22 編程語言

什么是控制反轉(Inversion of Control)?

控制反轉就是把傳統的控制邏輯委托給另一個類或框架來處理,客戶方只需實現具體的任務而不需要關心控制邏輯。

舉個例子,比如存在客戶方和服務方兩個類,客戶方需要調用服務方的函數來執行某個邏輯。在傳統的編程方式中,客戶方根據自己的需求直接去調用服務方的函數從而達到目的。而控制反轉,則是把控制邏輯交給服務方,服務方提供了一個控制流的框架,具體的內容需要由客戶方來填充,也就是說對流程的控制反轉了,現在是服務方調用客戶方。據說好萊塢有句名言 Don't call us, we'll call you,差不多就是這個意思。以上的服務方也可以是庫代碼或者框架。

在 iOS 開發中,有一個非常常見的控制反轉的實現,可能很多人都沒有意識到這個就是控制反轉,那就是 completionHandler,或者說 callback。

API.request(completion: { data in
    handleData(data)
})

在這個例子中,業務方只需要關系拿到數據以后干什么,而不關心 completion 的調用時機,把 completion 的調用委托給了網絡庫,這就是控制反轉。

控制反轉可以讓主要任務和控制邏輯分離,可以提升代碼的模塊性和擴展性,讓代碼松耦合,并可以讓寫測試代碼變得簡單。

常見的控制反轉的實現有:

  • 服務定 位 器(Service Locator)
  • 依賴注入
  • 上下文查找(Contextualized lookup)
  • 模板方法(Template method)
  • 策略模式(Strategy design pattern)

本文僅討論依賴注入這種實現,暫不討論其他的實現。

什么是依賴注入?

依賴注入是控制反轉的一種具體實現。它在類的外部創建這個類的依賴對象,然后通過某個方式把這個依賴對象提供給類。通過依賴注入,把依賴對象的創建和綁定都移動到了類的外部。

先看下面的例子:

class Car {
    var tyres: [Tyre]
    init() {
        let tyre1 = Tyre()
        let tyre2 = Tyre()
        let tyre3 = Tyre()
        let tyre4 = Tyre()
        tyres = [tyre1, tyre2, tyre3, tyre4]
    }
}

這個例子中構建了一個汽車對象,汽車對象的構建需要拼裝 4 個輪胎。這個代碼的缺點顯而易見,就是輪胎的創建邏輯和汽車本身耦合了。當我們想換成另一種輪胎時,或者 Tyre 類調整了實現在構造時添加了一個參數,都必須改動 Car 類中的代碼。

用依賴注入的方式,把依賴對象的創建和綁定挪到類外部,就能解決這類問題。

class Car {
    var tyres: [Tyre]
    init(types: [Tyre]) {
        self.types = types
    }
}

再舉個例子,App 開發中常見的網絡請求 -> 數據處理 -> 數據渲染流程,傳統方式開發如下:

// DataViewModel.swift
func loadData() {
    API.requestData(id: 2222, completion: { data in 
        self.handleData(data)
    })
}

這樣的代碼是無法測試的,因為 ViewModel 和具體的網絡請求實現耦合了。為了讓 loadData 這個方法可以被測試,應該抽象一個網絡請求的接口,然后從外部注入這個接口的實現。如下代碼:

protocol Networking {
    func requestData(id: Int, completion: (Data) -> Void)
}

讓 DataViewModel 擁有一個需要注入的屬性對象:

class DataViewModel {
    let networking: Networking
    init(networking: Networking) {
        self.networking = networking
    }
}

loadData 方法修改如下:

func loadData(completion: (() -> Void)?) {
    networking.requestData(id: 2222, completion: { data in
        self.handleData(data)
    })
}

這樣把具體的網絡請求實現轉移到外部注入,在測試的時候就可以簡單的實現一個模擬的網絡請求,這樣就可以寫出測試代碼:

func testLoadData() {
    let networking = MockNetworking()
    let viewModel = DataViewModel(networking: networking)
    let expectation = XCTestExpectation()
    viewModel.loadData {
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: .infinity)
    XCTAssertTrue(viewModel.data.xxxx)
}

依賴注入最大的優點就是實現了類之間的解耦。什么叫解耦,解耦就是兩個類之間雖然存在一些依賴關系,但是當其中任何一個類的實現發生改變時,另一個類的實現完全不受影響,解耦本身是通過抽象接口來實現的,因此依賴注入也需要抽象接口,并且依賴注入將依賴的創建轉移到了客戶類的外部,把依賴的創建邏輯也和客戶類本身解耦。這樣無論是替換依賴對象的實現類,還是替換依賴對象類實現中的某個部分,客戶方類都不需要做任何修改。

依賴注入的種類

依賴注入主要通過初始化器注入、屬性注入、方法注入、接口注入等方式來進行注入。

初始化器注入

初始化器注入就是通過初始化方法的參數來給對象提供依賴。初始化器注入是最常用的注入方式,它簡單直觀,當一個對象依賴的對象的生命周期和對象自身相同時,使用初始化器注入是最好的方式。

class ClientObject {
    private var dependencyObject: DependencyObject
    init(dependencyObject: DependencyObject) {
        self.dependencyObject = dependencyObject
    }
}

屬性注入

屬性注入是通過對象的公開屬性來給對象提供依賴,也可以叫 setter 注入。一般在無法使用初始化器注入時(例如使用了 Storyboard),或者依賴對象的生命周期小于對象時使用。

public class ClientObject {
    public var dependencyObject: DependencyObject
}
let client = ClientObject()
client.dependencyObject = DefaultDependencyObject()

方法注入

方法注入的方式是對象需要實現一個接口,這個接口中聲明了能給對象提供依賴的方法。注入器通過調用這個方法來給對象提供依賴。方法注入也可以叫接口注入。

protocol DependencyObjectProvider {
    func dependencyObject() -> DependencyObject
}

有時客戶方只在某些特定的條件下才需要使用依賴,這時可以用方法注入,客戶方僅僅在需要使用依賴的時候才會去調用方法來創建依賴對象,這樣避免了客戶方在不使用依賴的時候依賴對象也被創建出來占用內存空間的問題。這一點也可以通過注入一個代碼塊來實現:

init(dependencyBuilder: () -> DependencyObject) {
    self.dependencyBuilder = dependencyBuilder
}

依賴注入容器

依賴注入要求對象的依賴在對象外部創建并通過某種方式注入到對象中。如果每次創建對象時都去創建一遍對象的依賴,會讓代碼變得重復和復雜,當對象的依賴調整時,每個地方都需要做改動。因此通常使用依賴注入時,也需要一個依賴注入容器(Dependency Injection Container)。

依賴注入容器用來統一地管理依賴對象的創建和生命周期,也可以根據需要給對象注入依賴。

依賴注入容器要提供以下的功能:

  • 注冊(Register):容器需要知道對于一個特定的類型,應該怎樣構建它的依賴,容器內會保存類型-依賴的映射信息,并提供接口可以向容器添加這個類型信息,這個操作就是注冊。
  • 解析(Resolve):當使用依賴注入容器時,就不需要手動創建依賴了,而是讓容器幫我們做這件事情。容器需要提供一個方法來根據類型得到一個對象,容器會創建好這個對象的所有依賴,調用方即可無需關心依賴,直接使用這個對象即可。
  • 處置(Dispose):容器需要管理依賴對象的生命周期,并在依賴對象的生命周期結束時處置它。

實現一個簡單的依賴注入容器

有很多第三方依賴注入框架實現了依賴注入的功能,例如 Swift 語言的 Swinject。我們也可以自己實現一個依賴注入容器。

按照依賴注入容器的定義,并借助 Swift 的泛型和協議,可以定義以下協議:

protocol DIContainer {
    func register<Component>(type: Component.Type, component: Any)
    func resolve<Component>(type: Component.Type) -> Component?
}

具體實現如下:

final class DefaultDIContainer: DIContainer {
    static let shared = DefaultDIContainer()
    private init() {}
    var components: [String: Any] = [:]
    func register<Component>(type: Component.Type, component: Any) {
        components["\(type)"] = component
    } 
    func resolve<Component>(type: Component.Type) -> Component? {
        return components["\(type)"] as? Component
    }
}

有了這個 DIContainer,在使用時可以選擇兩種方式,一種是在外部 resolve 對象并注入。

let object = DIContaienr.shared.resolve(Data.self)
let viewModel = ViewModel(dependencyObject: object)

一種是在初始化方法的參數默認值中 resolve:

class ViewModel {
    init(dependencyObject: DependencyObject = DIContainer.shared.resolve(Data.self))
    self.dependencyObject = dependencyObject
}

這兩種方式各有一些使用場景,可以根據具體情況選用。

以上的 DIContainer 只是一個簡單的實現,結合具體需求,可以添加上線程安全、Storyboard 注入,自動解析等功能,可參考 Swinject。

總結

依賴注入在很多領域都是常見的設計模式,例如 Java Spring 等,無論做哪個方向的開發,依賴注入都是一定要掌握的。在合適的時候使用依賴注入,可以讓代碼解耦,提高代碼的可測試性、可擴展性。

參考資料

  • www.tutorialsteacher.com/ioc/ioc-con…
  • en.wikipedia.org/wiki/Invers…
  • en.wikipedia.org/wiki/Depend…

原文鏈接:https://juejin.cn/post/7193220280993447992

欄目分類
最近更新