iOS 之 Swinject 入门教程

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 包含一个UIViewControllerBitcoinViewController,在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()
}

解析代码:

  1. 创建请求 Bitcoin 价格的URLRequest

  2. 创建URLSessionDataTask网络请求 Bitcoin 价格任务。如果 HTTP 请求成功,会返回一段 JSON 格式的数据:

    {
      "data": {
        "base": "BTC",
        "currency": "USD",
        "amount": "15840.01"
      }
    }
    
  3. 每个 HTTP 请求后都应该检测错误,这里也不例外。

  4. 使用 JSONDecoder 解析 JSON 数据,并 map 到 modle 对象PriceResponse

  5. 异步传输 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)提高了代码质量。

猜你喜欢

转载自blog.csdn.net/box_kun/article/details/107229629