许多年前, 小梁进了他的第一家公司, 不久迎来了他的第一个项目, 他翻了下苹果的文档决定用 URLSession 来调后台 API, 于是他在每个需要和服务器交互的地方写下了如下代码:
- class AViewController: UIViewController {
- func loadData() {
- let url: URL = "https://api.com/path?query=key"
- let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
- if let error = error {
- self.failure(error)
- } else {
- self.success(data ?? Data())
- }
- }
- task.resume()
- }
- func failure(_ error: Error) {
- }
- func success(_ data: Data) {
- }
- }
经过几期迭代, 产品找到小梁同学说:"把我们项目所有的网络请求超时时间设成 30s, 并在所有的请求头里添加指定参数". 小梁同学可以对每一个 loadData() 函数进行修改, 但是他现在已经编码一年, 可以说得上是一个有些许经验的程序员了, 于是他决定对项目的网络请求部分进行重构.
第一步, 封装 DataLoader 类来集中管理网络请求:
- class DataLoader {
- enum Result {
- case success(Data)
- case failure(Error)
- }
- func load(_ url: URL, completion: @escaping (Result) -> Void ) {
- let configuration = URLSessionConfiguration.default
- configuration.timeoutIntervalForRequest = 30
- configuration.httpAdditionalHeaders = [
- "Accept-Encoding": "acceptEncoding",
- "Accept-Language": "acceptLanguage",
- "User-Agent": "userAgent"
- ]
- let session = URLSession(configuration: configuration)
- let task = session.dataTask(with: url) { (data, response, error) in
- if let error = error {
- return completion(.failure(error))
- }
- completion(.success(data ?? Data()))
- }
- task.resume()
- }
- }
- class AViewController: UIViewController {
- func loadData() {
- let url: URL = "https://api.com/path?query=key"
- DataLoader().load(url) { (result) in
- switch result {
- case .failure(let error):
- self.failure(error)
- case .success(let data):
- self.success(data)
- }
- }
- }
- func failure(_ error: Error) {
- }
- func success(_ data: Data) {
- }
- }
到这里, 其实已经可以满足了产品提的需求. 但是小梁毕竟想表现的更 "老鸟" 一点, 而且也想让代码更具有'swif style'于是便进行了提取协议
第二步, 提取协议, 这一步的目的是把请求部件移到一个协议中. 代码如下
- protocol NetworkEngine {
- typealias Handler = (Data?, URLResponse?, Error?) -> Void
- func request(for url: URL, completion: @escaping Handler)
- }
- extension URLSession: NetworkEngine {
- typealias Handler = NetworkEngine.Handler
- func request(for url: URL, completion: @escaping Handler) {
- let task = dataTask(with: url, completionHandler: completion)
- task.resume()
- }
- }
如您所见, URLSession 遵守 NetworkEngine 协议, 并封装了请求细节. 这样, 我们就可以专注于 NetworkEngineAPI.
第三步, 依赖项注入, 现在, 让我们 DataLoader 从之前更新我们使用新的 NetworkEngine 协议, 并将其作为依赖项注入. 我们将使用 URLSession.shared 默认参数, 以便我们可以保持向后兼容性和以前一样的便利性. 代码如下
- class DataLoader {
- enum Result: Equatable {
- case success(Data)
- case failure(Error)
- }
- static var defaultEngine: URLSession = {
- let configuration = URLSessionConfiguration.default
- configuration.timeoutIntervalForRequest = 30
- configuration.httpAdditionalHeaders = [
- "Accept-Encoding": "acceptEncoding",
- "Accept-Language": "acceptLanguage",
- "User-Agent": "userAgent"
- ]
- let session = URLSession(configuration: configuration)
- return session
- }()
- private let engine: NetworkEngine
- init(engine: NetworkEngine = defaultEngine) {
- self.engine = engine
- }
- func load(_ url: URL, completion: @escaping (Result) -> Void ) {
- engine.request(for: url) { (data, response, error) in
- if let error = error {
- return completion(.failure(error))
- }
- completion(.success(data ?? Data()))
- }
- }
- }
重构到这里, 小梁同学将利用 NetworkEngine 协议模拟测试, 以使他的测试快速, 可预测且易于维护. 于是他又定义了一个 Mock 类
- class NetworkEngineMock: NetworkEngine {
- typealias Handler = NetworkEngine.Handler
- var requestedURL: URL?
- func request(for url: URL, completion: @escaping Handler) {
- requestedURL = url
- let data = "Hello world".data(using: .utf8)
- completion(data, nil, nil)
- }
- }
到这里, 小梁觉得他功德圆满, 既重构了代码, 又完成了产品的需求.
#### 但是, 他真的功德圆满吗?
来源: https://juejin.im/post/5bfb41f8f265da612637d373