檢視控制器的依賴注入
Dependenct 注入介紹
應用程式由許多彼此協作的物件組成。物件通常依賴於其他物件來執行某些任務。當一個物件負責引用它自己的依賴項時,它會導致高度耦合,難以測試和難以更改的程式碼。
依賴注入是一種軟體設計模式,它實現控制的反轉以解決依賴關係。注入是將依賴關係傳遞給將使用它的依賴物件。這允許將客戶端的依賴關係與客戶端的行為分開,這允許應用程式鬆散耦合。
不要與上面的定義混淆 - 依賴注入只是意味著給物件賦予例項變數
就這麼簡單,但它提供了很多好處:
- 更容易測試你的程式碼(使用單元和 UI 測試等自動化測試)
- 當與面向協議的程式設計一起使用時,它可以很容易地改變某個類的實現 - 更容易重構
- 它使程式碼更加模組化和可重用
有三種最常用的方法可以在應用程式中實現依賴注入(DI):
- 初始化器注入
- 屬性注入
- 使用第三方 DI 框架(如 Swinject,Cleanse,Dip 或 Typhoon)
有一篇有趣的文章連結到更多關於依賴注入的文章,所以如果你想深入研究 DI 和 Inversion of Control 原則,請檢視它。
讓我們展示如何將 DI 與 View Controllers 一起使用 - 這是普通 iOS 開發人員的日常任務。
沒有 DI 的例子
我們將有兩個檢視控制器: LoginViewController 和 TimelineViewController 。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,你將看到好處並始終使用依賴注入。