一、背景:
近来在给deepLink功能添加单元测试,发现代码好些地方耦合严重,没办法写单元测试,通过学习发现可以使用依赖注入/控制反转的方式,把关键代码通过外部注入,从而进行单元测试。
二、依赖注入(Dependency Injection) / 控制反转(Inversion of Control)
用电脑和CPU的关系来说明一下:电脑的能力由CPU决定,电脑 依赖 CPU。
非依赖注入: 可理解为电脑和CPU是耦合在一起的,创建电脑的时候,就已经决定了使用何种CPU,也就是说电脑的性能已经不可改变。
依赖注入: 可理解为电脑为CPU提供了个接口,可以通过接口更换CPU,从而提升电脑的性能。电脑和CPU不再耦合在一起了。可以根据性能需求,更替不同的CPU。
-
非依赖注入
class CPU {} class Computer { let cpu: CPU = CPU() } //VC let compture = Computer() -
依赖注入
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的控制权被由自身控制反转为外部控制。
通过这个简单的例子,可以看出其实 依赖注入 和 控制反转 说的是同一件事情,只是站的角度不同而已。
三、非依赖注入和依赖注入某些场合下的对比:
-
哪天调整了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依赖其他类,而其他类又可能有各自的依赖,这样的话,使用依赖注入就相当有必要了。
四、实际开发中,使用依赖注入的例子:
-
打开
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())) } } }