Swinject Tutorial for iOS: 入门教程
我们通过一个简短教程来探索 Dependency Injection (DI),主要介绍一款 Swift 语言写的框架——Swinject。
在本教程中,您将通过 Swinject 探索依赖注入 (DI)。通过改进一个名为 Bitcoin Adventurer(Bitcoin 冒险家) 的 iOS 小程序来实现这一点,该程序可以显示当前比特币的价格。在阅读本教程时,您将重构应用程序,并完成单元测试。
依赖注入(DI)是一种组织代码的方法,目的使其依赖项由其他不同的对象提供,而不是由其本身提供,通过本教程的代码演示更容易理解一些。使用依赖注入技术可以让代码耦合更松散,便于单元测试和重构。
实现依赖注入技术并不一定非要使用第三方库来实现,但是使用 Swinject 可以让工作更简单,即使在代码复杂度不断升高时。
为什么使用依赖注入?
依赖注入技术依赖于一种称为控制反转的原理。其主要思想是,一段需要依赖关系的代码不会为自己创建依赖关系,而是将提供这些依赖关系的控制权交给更高的抽象对象。这些依赖关系通常被传递到对象的初始化代码中。
使用了 DI 框架中流行的模式: 依赖项注入 (DI) 容器。这种模式使依赖项的解析变得简单,即使代码复杂度增加。
在代码实践中,控制反转的主要好处是代码更改仍然是独立的。一个 DI Container 提供某些对象来支持控制主体倒置,而这些对象本身知道如何提供依赖关系。你所需要做的就是向容器请求你需要的这些对象! 听起来很难理解的样子,下面来看看代码实例。
开始吧
这里下载项目代码 (链接地址)。打开Bitcoin Adventurer.xcworkspace
,然后按 Command+R 运行项目。
当应用程序启动时,你会看到屏幕上显示的比特币的当前价格。点击 Refresh 会发出 HTTP 请求来检索最新的数据,这些数据被记录到 Xcode 控制台。比特币是一种易波动的加密货币,其价值经常波动,因此 Coinbase API 大约每 30 秒就有一个新的比特币价格可用。
您可能还注意到控制台记录了一个错误。您现在可以忽略它,因为您将在本教程的后面介绍它。
返回 Xcode 检查项目:
- 这个 app 包含一个
UIViewController
,BitcoinViewController
,在main.storyboard
中引用。 - 所有的网络层和数据逻辑层都位于 BitcoinViewController.swift 中。按照目前的代码,独立于 UIViewController 生命周期测试逻辑是很困难的,因为视图层与它的底层逻辑和依赖关系高度耦合。
-
我们已经通过 CocoaPods 为您添加了一个依赖项 Swinject。目前它还没有在你的任何 Swift 文件中使用,但这即将改变!
依赖注入 DI 和解耦合
如前所说的依赖关系定义,就是为另一个对象完成工作的一段代码,最好是由单独的对象提供的,或者说是注入一段代码来完成依赖关系建立。
来探索下Bitcoin Adventurer
项目代码中的依赖关系。
打开BitcoinViewController.swift
,可以看到有三个主要职责:网络请求、数据解析、格式化
网络请求和数据解析
大部分网络请求任务通过一个函数requestPrice()
private func requestPrice() { let bitcoin = Coinbase.bitcoin.path // 1. Make URL request 创建URL guard let url = URL(string: bitcoin) else { return } var request = URLRequest(url: url) request.cachePolicy = .reloadIgnoringCacheData // 2. Make networking request 发送网络请求 let task = URLSession.shared.dataTask(with: request) { data, _, error in // 3. Check for errors 检测错误 if let error = error { print("Error received requesting Bitcoin price: \(error.localizedDescription)") return } // 4. Parse the returned information 解析网络返回数据 let decoder = JSONDecoder() guard let data = data, let response = try? decoder.decode(PriceResponse.self, from: data) else { return } print("Price returned: \(response.data.amount)") // 5. Update the UI with the parsed PriceResponse 更新UI信息 DispatchQueue.main.async { [weak self] in self?.updateLabel(price: response.data) } } task.resume() } |
解析代码:
-
创建请求 Bitcoin 价格的
URLRequest
。 -
创建
URLSessionDataTask
网络请求 Bitcoin 价格任务。如果 HTTP 请求成功,会返回一段 JSON 格式的数据:{ "data": { "base": "BTC", "currency": "USD", "amount": "15840.01" } }
-
每个 HTTP 请求后都应该检测错误,这里也不例外。
-
使用 JSONDecoder 解析 JSON 数据,并 map 到 modle 对象
PriceResponse
-
异步传输 model 数据及更新 UI。
格式化
BitcoinViewController
中的updateLabel(price:)
函数任务是更新 Label 内容,确保正确显示比特币价格,包括整数的美元及小数的美分。
总结下,一个UIViewController
内包含了不同的业务逻辑,如网络请求、数据解析、格式化,而且他们紧紧的耦合在一起。很难独立于整个BitcoinViewController
对象测试它的任何部分,也很难在其他地方重用相同的逻辑。
That doesn’t sound good – can we fix this?
对于紧密耦合的另外一面就是建立松散耦合对象,可以轻松链接,轻松解除。
是时候重构BitcoinViewController
了,以便它为网络请求和数据解析职责创建单独的对象。完成之后,您将使用 Swinject 调整它们的使用,以实现真正的解耦组件。
剥离依赖关系
首先创建一个名为 Dependencies 的新文件夹。这将保存您将在本教程其余部分中提取的所有逻辑块。右键单击 Bitcoin Adventurer 文件夹并选择 New Group。然后将其名称设置为 Dependencies。
下面开始分离业务逻辑之旅吧!让代码更利于测试,更健壮,更漂亮。
剥离网络层逻辑
在 Dependencies 文件夹下创建一个文件:HTTPNetworking.swift。添加以下的代码,根据注释理解代码含义。
// 1.定义Networking协议,包含一个方法request(from:completion:),返回Data或者Error protocol Networking { typealias CompletionHandler = (Data?, Swift.Error?) -> Void func request(from: Endpoint, completion: @escaping CompletionHandler) } // 2.创建网络层HTTP协议的实现 struct HTTPNetworking: Networking { // 3.协议方法实现,创建网络请求,根据指定的网址 func request(from: Endpoint, completion: @escaping CompletionHandler) { guard let url = URL(string: from.path) else { return } let request = createRequest(from: url) let task = createDataTask(from: request, completion: completion) task.resume() } // 4.创建URLRequst的子方法 private func createRequest(from url: URL) -> URLRequest { var request = URLRequest(url: url) request.cachePolicy = .reloadIgnoringCacheData return request } // 5.发送网络请求的子方法 private func createDataTask(from request: URLRequest, completion: @escaping CompletionHandler) -> URLSessionDataTask { return URLSession.shared.dataTask(with: request) { data, httpResponse, error in completion(data, error) } } } |
好了,开始使用网络逻辑层代码,打开BitcoinViewController
,并在顶端三个IBOutles
下面添加如下代码:
let networking = HTTPNetworking() |
可以修改 requestPrice() 代码了,如下:
networking.request(from: Coinbase.bitcoin) { data, error in // 1. Check for errors 检测网络错误 if let error = error { print("Error received requesting Bitcoin price: \(error.localizedDescription)") return } // 2. Parse the returned information 解析网络返回的JSON数据 let decoder = JSONDecoder() guard let data = data, let response = try? decoder.decode(PriceResponse.self, from: data) else { return } print("Price returned: \(response.data.amount)") // 3. Update the UI with the parsed PriceResponse 更新UI DispatchQueue.main.async { [weak self] in self?.updateLabel(price: response.data) } } |
再次运行项目,依然可以正常工作。漂亮!你已经成功的剥离出网络层业务逻辑。然而为了最终的依赖注入,还有更多的解耦合要做。
剥离数据解析层逻辑
同样在 Dependencies 目录下建立一个文件:BitcoinPriceFetcher.swift。写入如下代码:
protocol PriceFetcher { func fetch(response: @escaping (PriceResponse?) -> Void) } struct BitcoinPriceFetcher: PriceFetcher { let networking: Networking // 1. Initialize the fetcher with a networking object init(networking: Networking) { self.networking = networking } // 2. Fetch data, returning a PriceResponse object if successful func fetch(response: @escaping (PriceResponse?) -> Void) { networking.request(from: Coinbase.bitcoin) { data, error in // Log errors if we receive any, and abort. if let error = error { print("Error received requesting Bitcoin price: \(error.localizedDescription)") response(nil) } // Parse data into a model object. let decoded = self.decodeJSON(type: PriceResponse.self, from: data) if let decoded = decoded { print("Price returned: \(decoded.data.amount)") } response(decoded) } } // 3. Decode JSON into an object of type 'T' private func decodeJSON<T: Decodable>(type: T.Type, from: Data?) -> T? { let decoder = JSONDecoder() guard let data = from, let response = try? decoder.decode(type.self, from: data) else { return nil } return response } } |
注意:PriceFetcher 协议定义了一个方法: 一个执行获取并返回 PriceResponse 对象的方法。这个 “fetch” 可以从任何数据源发起请求,而不一定是 HTTP 请求。当您开始编写单元测试时,需要 Mock 一些本地数据,这将成为该协议的一个重要特征。这里 fetch 发起的请求使用的是新创建的网络协议。
现在有个一个更具体的抽象层逻辑来获取比特币价格,是时候再出重构BitcoinViewController
来使用它了。
替换代码:
let networking = HTTPNetworking() |
为:
let fetcher = BitcoinPriceFetcher(networking: HTTPNetworking()) |
然后再出修改requestPrice()
的实现代码:
private func requestPrice() { fetcher.fetch { response in guard let response = response else { return } DispatchQueue.main.async { [weak self] in self?.updateLabel(price: response.data) } } } |
现在上面的代码看上去更简洁和易读,因为把繁重的任务交给依赖项BitcoinPriceFetcher
去处理了。
再次运行项目,依然可以正常工作,恭喜你,你通过使用依赖注入技术(DI)提高了代码质量。