檢視控制器的依賴注入

Dependenct 注入介紹

應用程式由許多彼此協作的物件組成。物件通常依賴於其他物件來執行某些任務。當一個物件負責引用它自己的依賴項時,它會導致高度耦合,難以測試和難以更改的程式碼。

依賴注入是一種軟體設計模式,它實現控制的反轉以解決依賴關係。注入是將依賴關係傳遞給將使用它的依賴物件。這允許將客戶端的依賴關係與客戶端的行為分開,這允許應用程式鬆散耦合。

不要與上面的定義混淆 - 依賴注入只是意味著給物件賦予例項變數

就這麼簡單,但它提供了很多好處:

  • 更容易測試你的程式碼(使用單元和 UI 測試等自動化測試)
  • 當與面向協議的程式設計一起使用時,它可以很容易地改變某個類的實現 - 更容易重構
  • 它使程式碼更加模組化和可重用

有三種最常用的方法可以在應用程式中實現依賴注入(DI):

  1. 初始化器注入
  2. 屬性注入
  3. 使用第三方 DI 框架(如 Swinject,Cleanse,Dip 或 Typhoon)

有一篇有趣的文章連結到更多關於依賴注入的文章,所以如果你想深入研究 DI 和 Inversion of Control 原則,請檢視它。

讓我們展示如何將 DI 與 View Controllers 一起使用 - 這是普通 iOS 開發人員的日常任務。

沒有 DI 的例子

我們將有兩個檢視控制器: LoginViewControllerTimelineViewController 。LoginViewController 用於登入,成功後,它將切換到 TimelineViewController。兩個檢視控制器都依賴於 FirebaseNetworkService

LoginViewController

class LoginViewController: UIViewController {

    var networkService = FirebaseNetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

TimelineViewController

class TimelineViewController: UIViewController {

    var networkService = FirebaseNetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func logoutButtonPressed(_ sender: UIButton) {
        networkService.logutCurrentUser()
    }
}

FirebaseNetworkService

class FirebaseNetworkService {

    func loginUser(username: String, passwordHash: String) {
        // Implementation not important for this example
    }
    
    func logutCurrentUser() {
        // Implementation not important for this example
    }
}

這個例子很簡單,但我們假設你有 10 個或 15 個不同的檢視控制器,其中一些還依賴於 FirebaseNetworkService。在某些時候,你希望使用公司的內部後端服務將 Firebase 更改為後端服務。為此,你必須瀏覽每個檢視控制器並使用 CompanyNetworkService 更改 FirebaseNetworkService。如果 CompanyNetworkService 中的某些方法發生了變化,那麼你將需要做很多工作。

單元和 UI 測試不是本示例的範圍,但如果你希望將檢視控制器與緊密耦合的依賴關係進行單元測試,那麼你將很難這樣做。

讓我們重寫這個例子,並將網路服務注入我們的檢視控制器。

依賴注入的示例

為了充分利用依賴注入,讓我們在協議中定義網路服務的功能。這樣,依賴於網路服務的檢視控制器甚至不必知道它的實際實現。

protocol NetworkService {
    func loginUser(username: String, passwordHash: String)
    func logutCurrentUser()
}

新增 NetworkService 協議的實現:

class FirebaseNetworkServiceImpl: NetworkService {
    func loginUser(username: String, passwordHash: String) {
        // Firebase implementation
    }
    
    func logutCurrentUser() {
        // Firebase implementation
    }
}

讓我們更改 LoginViewController 和 TimelineViewController 以使用新的 NetworkService 協議而不是 FirebaseNetworkService。

LoginViewController

class LoginViewController: UIViewController {

    // No need to initialize it here since an implementation
    // of the NetworkService protocol will be injected
    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

TimelineViewController

class TimelineViewController: UIViewController {

    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func logoutButtonPressed(_ sender: UIButton) {
        networkService?.logutCurrentUser()
    }
}

現在,問題是:我們如何在 LoginViewController 和 TimelineViewController 中注入正確的 NetworkService 實現?

由於 LoginViewController 是起始檢視控制器,並且每次應用程式啟動時都會顯示,因此我們可以在 AppDelegate 中注入所有依賴項。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // This logic will be different based on your project's structure or whether
    // you have a navigation controller or tab bar controller for your starting view controller
    if let loginVC = window?.rootViewController as? LoginViewController {
        loginVC.networkService = FirebaseNetworkServiceImpl()
    }
    return true
}

在 AppDelegate 中,我們只是引用第一個檢視控制器(LoginViewController)並使用屬性注入方法注入 NetworkService 實現。

現在,下一個任務是在 TimelineViewController 中注入 NetworkService 實現。最簡單的方法是在 LoginViewController 轉換到 TimlineViewController 時執行此操作。

我們將在 LoginViewController 中的 prepareForSegue 方法中新增註入程式碼(如果你使用不同的方法來瀏覽檢視控制器,請將注入程式碼放在那裡)。

我們的 LoginViewController 類現在看起來像這樣:

class LoginViewController: UIViewController {
    // No need to initialize it here since an implementation
    // of the NetworkService protocol will be injected
    var networkService: NetworkService?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "TimelineViewController" {
            if let timelineVC = segue.destination as? TimelineViewController {
                // Injecting the NetworkService implementation
                timelineVC.networkService = networkService
            }
        }
    }
}

我們完成了,就這麼簡單

現在想象一下,我們希望將 NetworkService 實現從 Firebase 切換到我們自定義公司的後端實現。我們所要做的就是:

新增新的 NetworkService 實現類:

class CompanyNetworkServiceImpl: NetworkService {
    func loginUser(username: String, passwordHash: String) {
        // Company API implementation
    }
    
    func logutCurrentUser() {
        // Company API implementation
    }
}

使用 AppDelegate 中的新實現切換 FirebaseNetworkServiceImpl:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // This logic will be different based on your project's structure or whether
        // you have a navigation controller or tab bar controller for your starting view controller
        if let loginVC = window?.rootViewController as? LoginViewController {
            loginVC.networkService = CompanyNetworkServiceImpl()
        }
        return true
    }

就是這樣,我們已經切換了整個網路服務協議的底層實現,甚至沒有觸及 LoginViewController 或 TimelineViewController。

由於這是一個簡單的示例,你現在可能看不到所有好處,但如果你嘗試在專案中使用 DI,你將看到好處並始終使用依賴注入。