iOS 依賴注入/控制反轉 + 實際項目的運用

1、背景:

近來在給deepLink功能添加單元測試,發現代碼好些地方耦合嚴重,沒辦法寫單元測試,經過學習發現可使用依賴注入/控制反轉的方式,把關鍵代碼經過外部注入,從而進行單元測試。typescript

2、依賴注入(Dependency Injection) / 控制反轉(Inversion of Control)

用電腦和CPU的關係來講明一下:電腦的能力由CPU決定,電腦 依賴 CPUapi

非依賴注入: 可理解爲電腦和CPU是耦合在一塊兒的,建立電腦的時候,就已經決定了使用何種CPU,也就是說電腦的性能已經不可改變。markdown

依賴注入: 可理解爲電腦爲CPU提供了個接口,能夠經過接口更換CPU,從而提高電腦的性能。電腦和CPU再也不耦合在一塊兒了。能夠根據性能需求,更替不一樣的CPU。網絡

  • 非依賴注入ide

    class CPU {}
    
    class Computer {
        let cpu: CPU = CPU()
    }
    
    //VC
    let compture = Computer()
    複製代碼
  • 依賴注入post

    class CPU {}
    
    class Computer {
        var cpu: CPU?
        
        init(cpu: CPU) {
            self.cpu = cpu
        }
    }
    
    //VC
    let cpu = CPU()
    let compture = Computer(cpu: cpu)
    複製代碼

依賴注入: 電腦和CPU再也不是強依賴關係。CPU是由外部給予電腦的,電腦和CPU有依賴,可是這個依賴是外部給予,所以咱們能夠說CPU是由外部注入給他的。性能

控制反轉: 而反過來講,電腦搭配何種CPU,具有何種性能,不是他內部自身控制的,而是由外部控制的,外部來決定電腦該具有什麼性能,因此CPU的控制權被由自身控制反轉爲外部控制。單元測試

經過這個簡單的例子,能夠看出其實 依賴注入 和 控制反轉 說的是同一件事情,只是站的角度不一樣而已。學習

3、非依賴注入和依賴注入某些場合下的對比:

  • 哪天調整了CPU類的初始化方法,須要傳個品牌名稱:測試

    class CPU {
        var name: String
        init(name: String) {
            self.name = name
        }
    }
    複製代碼
    • 非依賴注入:須要修改Computer中的cpu變量。
    class Computer { 
        let cpu: CPU = CPU(name: "Intel") 
    }
    
    let compture = Computer()
    複製代碼
    • 依賴注入:只須要在VC中,建立Computer對象時,注入CPU對象便可。
    class Computer {
        var cpu: CPU?
        
        init(cpu: CPU) {
            self.cpu = cpu
        }
    }
    
    let cpu = CPU(name: "Intel")
    let compture = Computer(cpu: cpu))
    複製代碼
  • 想在電腦上使用不一樣的品牌的CPU:

    class CPU1: CPU {}
    複製代碼
    • 非依賴注入:又要修改Computer類內部的cpu變量
    class Computer { 
        let cpu: CPU1 = CPU1(name: "AMD") 
    }
    
    let compture = Computer()
    複製代碼
    • 依賴注入:無需修改Computer類,只須要在VC中修改一下便可
    class Computer {
        var cpu: CPU?
    
        init(cpu: CPU) {
            self.cpu = cpu
        }
    }
    
    let cpu = CPU1(name: "AMD")
    let compture = Computer(cpu: cpu)
    複製代碼
  • 核心優勢:利於自動化測試。

    給Computer類添加introduction()方法,並根據不一樣的CPU品牌去測試該方法:

    • 非依賴注入:改不了Computer裏的cpu變量,只能測當前1種品牌。作不到自動化測試。
    class Computer { 
        let cpu: CPU = CPU(name: "Intel") 
        func introduction() -> String {
            "I use \(cpu.name) cpu"
        }
    }
    
    func testIntelCPU() {
        let computer = Computer()
        XCTAssertEqual(computer.introduction(), "I use Intel cpu")
    }
    複製代碼
    • 依賴注入:傳入不一樣品牌的CPU,便可自動化測試全部品牌
    class Computer {
        var cpu: CPU?
    
        init(cpu: CPU) {
            self.cpu = cpu
        }
        
        func introduction() -> String {
            "I use \(cpu.name) cpu"
        }
    }
    
    func testIntelCPU() {
        let cpu = CPU(name: "Intel")
        let computer = Computer(cpu: cpu)
        XCTAssertEqual(computer.introduction(), "I use Intel cpu")
    }
    
    func testAMDCPU() {
        let cpu = CPU(name: "AMD")
        let computer = Computer(cpu: cpu)
        XCTAssertEqual(computer.introduction(), "I use AMD cpu")
    }
    複製代碼

    Computer依賴CPU,假如CPU中又有其餘對象,即CPU依賴其餘類,而其餘類又可能有各自的依賴,這樣的話,使用依賴注入就至關有必要了。

4、實際開發中,使用依賴注入的例子:

  • 打開MainViewController頁面時,默認顯示LoadingView,此時發起網絡請求,根據請求結果顯示相應的頁面:

    • 默認顯示LoadingView
    • 網絡請求成功,顯示SuccessView
    • 網絡請求失敗,顯示FailureView
    final class MainViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
        
            view = LoadingView()
            
            //網絡請求
            client.fetchSomething(.cacheFirst)
                .deliverOnUIQueue()
                .onComplete { result in
                    switch result {
                    case .success:
                        view = SuccessView()
                    case .failure(let error):
                        view = FailureView()
                    }
                }
        }
    }
    複製代碼
  • 爲了測試3種狀態下的頁面顯示狀況,因此須要將網絡請求部分做爲依賴注入,因此創建一個協議MainPageProvider,原代碼修改成:

    protocol MainPageProvider: AnyObject {
        func loadData(completion: @escaping (Result<(), Error>) -> Void)
    }
    
    final class MainViewController: UIViewController {
        lazy var mainPageProvider: MainPageProvider = self
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view = LoadingView()
    
            //網絡請求
            mainPageProvider.loadData { result in
                switch result {
                case .success:
                    view = SuccessView()
                case .failure(let error):
                    view = FailureView()
                }
            }
        }
    }
    
    extension MainViewController: MainPageProvider {
        func loadData(completion: @escaping (Result<(), Error>) -> Void) {
            client.fetchSomething(.cacheFirst)
                .deliverOnUIQueue()
                .onComplete { result in
                    switch result {
                    case .success:
                        completion(.success(()))
                    case .failure(let error):
                        completion(.failure(error))
                    }
                }
        }
    }
    複製代碼
  • 在單元測試中,建立一個Mock類MockMainPageProvider遵循MainPageProvider協議,從而自定義協議方法,將網絡請求部分做爲依賴注入到MainViewController中,這樣就能夠自動化測試3種view的顯示狀況了。

    final class MainViewControllerTests: XOTestCase {
        var mockMainPageProvider: MockMainPageProvider!
        var mainViewController: MainViewController!
        
        override func setUp() {
            super.setUp()
            mockMainPageProvider = MockMainPageProvider()
            mainViewController.mainPageProvider = mockMainPageProvider
        }
        
        override func tearDown() {
            mockMainPageProvider = nil
            mainViewController = nil
            super.tearDown()
        }
        
        func testMainPageLoadingView() {
            mockMainPageProvider.state = .loading
            mainViewController.viewDidLoad()
            XCTAssertTrue(mainViewController.view is LoadingView)
        }
        
        func testMainPageSuccessView() {
            mockMainPageProvider.state = .success
            mainViewController.viewDidLoad()
            XCTAssertTrue(mainViewController.view is SuccessView)
        }
        
        func testMainPageSuccessView() {
            mockMainPageProvider.state = .failure
            mainViewController.viewDidLoad()
            XCTAssertTrue(mainViewController.view is FailureView)
        }
    }
    
    private class MockMainPageProvider: MainPageProvider {
        enum State {
            case loading, success, failure
        }
        var state: State = .loading
        func loadData(completion: (Result<(), Error>) -> Void) {
            switch state {
            case .loading:
                break
            case .success:
                completion(.success(()))
            case .failure:
                completion(.failure(NSError()))
            }
        }
    }
    複製代碼

5、參考文章:

相關文章
相關標籤/搜索