简单的3步,重构我们的代码

1,296 阅读2分钟

许多年前,小梁进了他的第一家公司,不久迎来了他的第一个项目,他翻了下苹果的文档决定用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)
    }
}

到这里,小梁觉得他功德圆满,既重构了代码,又完成了产品的需求。

####但是,他真的功德圆满吗?