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、参考文章:

相关文章
相关标签/搜索