簡單的3步,重構我們的程式碼

快樂的小樑同學發表於2018-11-26

許多年前,小樑進了他的第一家公司,不久迎來了他的第一個專案,他翻了下蘋果的文件決定用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)
    }
}

複製程式碼

到這裡,小樑覺得他功德圓滿,既重構了程式碼,又完成了產品的需求。

####但是,他真的功德圓滿嗎?

相關文章