MENU

任务分离:创建可测试的网络层

作者:Fernando Martín Ortiz 翻译:BigNerdCoding 如有错误欢迎指出。原文链接

最近几年出现了很多设计良好的 iOS 开发架构,这些架构在开发人员中得到了广泛的传播和应用。虽然这些架构并不是绝对完美的但总的来说这些设计观念都非常实用,而且这些架构出发点惊人的相似:分离代码的业务逻辑和界面呈现。接下来我将介绍的概念具有万金油特征,无论以后以使用何种架构开发,你都可以将其应用其中。

不错的常规网络层

在分享我的观点之前,首先让我们来看看网络层的常规实现。

我研究过很多网络层的代码,其中的大多数都有 NetworkManagerConnectionManager 或者其他类似的设计。通过这个单独的类实现对 APP 中所有网络请求的管理。虽然这些管理类在代码能够很好的完成任务,但这个设计却违背了软件设计中的核心理念之一:单一职责原则

ConnectionManager 中包含了太多的职责和功能,这不能被看作是一个好的编程实践。而且该类大多数情况下还会以单例类的形式出现。并不是说单例模式一定就有问题,而是单例无法进行依赖注入,并且也会给测试带来诸多不便。

Networking Layer is commonly implemented as a singleton

上图的设计实现非常普遍,甚至在 MVVM、MVP 架构中也存在类似的代码实践。

不同的方式

数据访问层代码实现方式多种多样,我们先看看网络请求的过程:

Steps involved in a network call

如上图所示,一个完整的网络请求最少应该有以下三个步骤:

  1. 创建网络请求:包含设置 URL 链接、网络方法、请求参数、HTTP头部内容

  2. 网络请求的调度:这是最为重要的一步。在上一步中我们设置好了的网络请求必须在这一步中使用 URLSession 或者其封装层(例如:Alamofire)进行调度。

  3. 解析获取的数据:将该过程从前面两者中分离出来是非常重要的。在这一步中我们需要对获取的 JSON 或者 XML 数据解析为一个数据实体 Model。

如果你真想创建一个清晰、可测试的的网络层架构的话,上诉步骤就需要以独立对象分别进行代码实现。

Three Step Layer

对象详解如下:

  1. Request:一个 Request 对象包含了网络请求中需要配置的所有信息。Request 以类、结构体的形式对单个请求进行配置,一个请求对应一个 Request 对象。

  2. NetworkDispatcher:NetworkDispatcher 负责处理网络请求并返回响应的对象。另外 NetworkDispatcher 的实现形式是协议。你应该面向该协议进行编码而不应该是具体的类或者结构体,更不能以单例模式对其进行实现。这样做的原因是:我们可以应用 mock 测试的方法将其替换成 MockNetworkDispatcher 对象,该对象不执行任何网络请求而是从一个 JSON 文件得到响应数据,这样久很自然的构建了一个可测试的框架。

  3. NetworkTask:NetworkTask 是范型类 Task 的子类。Task 的任务是以同步或者异步的方式从一个输入类型中得到一个输出类型,后面会详细介绍。你可以用 RxSwift, ReactiveCocoa, Hydra, Microfutures, FOTask 等类库来对 Task 进行实现,当然也可以直接使用闭包。选择随你,重要的是设计概念而不是具体的实现细节。

Request 的实现

Request 对象包含了创建 URLRequest 所需的所有配置信息。

下面是代码实现的示例:

//
//  Request.swift
//
//  Created by Fernando Ortiz on 2/12/17.
//
import Foundation

enum HTTPMethod: String {
    case get, post, put, patch, delete
}

protocol Request {
    var path        : String            { get }
    var method      : HTTPMethod        { get }
    var bodyParams  : [String: Any]?    { get }
    var headers     : [String: String]? { get }
}

extension Request {
    var method      : HTTPMethod        { return .get }
    var bodyParams  : [String: Any]?    { return nil }
    var headers     : [String: String]? { return nil }
}

简单的不能再简单了。这段代码的重点就是将每一个请求都作为独立对象分离开来。当然你可以像 Moya 一样使用枚举来进行实现,一切都取决于个人的的风格偏爱。我个人更喜欢使用面向对象的风格,这样我可以实现一个 BaseRequest 类并为每一个具体的请求实现一个子类,如: AuthenticatedRequestGetAllUsersRequestLoginRequest

NetworkDispatcher 的实现

NetworkDispatcher 是一个管理网络请求的组件。

注意:在文章的中我会使用 RxSwift 最为示例,但是你完全可以做出自己的选择。

//
//  NetworkDispatcher.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//  Copyright © 2017 Fernando Martín Ortiz. All rights reserved.
//
import Foundation
import RxSwift

protocol NetworkDispatcher {
    func execute(request: Request) -> Observable<Any>
}

NetworkDispatcher 的唯一职责就是执行网络请求返回响应数据。

这里使用协议而不是特定实现的原因是基于协议的实现可以更容易地进行互换。 您可以创建一个 MockNetworkDispatcher,它实际上不执行任何“网络”操作,而是从JSON文件返回一个响应,这让网络测试变的更容易。

任务分离

Task 负责单个业务逻辑操作的简单对象。例如,它可能是一些从后台获取用户信息,用户登录,用户注册类似的业务操作。虽然任务可能是同步或者异步的,但是对客户端而言这些都应该是透明的。这里我使用了 RxSwift 中的抽象 Observable,当然 Promise, Signal, Future 或者简单的闭包也可以实现。

Task 的简单实现如下:

//
//  Task.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//
import Foundation
import RxSwift

class Task<Input, Output> {
    func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}

这里我使用了简单直接的面向对象风格,你也可以使用 associated type 这样的新风格。我之所以选择面向对象风格是因为它更为直接也更容易进行代码实现。

每个 Task 都需要两个范型参数:一个 Input 类型,一个 Output 类型。Task 对输入进行处理并返回数据,该返回数据可以进行 Observable 形式的抽象封装。

接下来我们声明具体类来进行网络操作:

//
//  NetworkTask.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//
import Foundation
import RxSwift

class NetworkTask<Input: Request, Output>: Task<Input, Output> {
    let dispatcher: NetworkDispatcher

    init(dispatcher: NetworkDispatcher) {
        self.dispatcher = dispatcher
    }

    override func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}

从上面代码中我们能清晰的发现:NetworkTask 中有两个范型 Input 和 Output,值得注意的是前者必须为 Request 类型对象。 NetworkTask 实例化时必须传入 NetworkDispatcher 对象,这样当我们想要进行测试的时候,你就可以很方便的传入一个MockNetworkDispatcher 对象。

架构回顾

以这种架构方式处理业务逻辑有助于减少系统耦合,在降低的系统复杂性的同时也增强了系统的可测试性。

架构的设计可以用下图简单表示:

Task based network layer

总结

通过多个独立对象对代码中的业务逻辑进行分离是软件设计中好的实践,它给我们带来的更易测试的架构。另外该设计降低了代码复杂度并且独立于你使用的其他架构。该设计可以应用于 ViewModel, Presenter, Interactor, Store 这样的架构背后,获取其他任何分离业务逻辑和表现逻辑的架构中。

我希望本文能够真正的对你起到帮助。如果你有任何疑问或者更好的方案,请在下方留下你的观点。

标签: 网络, 架构