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)提高了代码质量。
那么,下一篇我们一起看看单元测试。
~ END ~