許多年前,小樑進了他的第一家公司,不久迎來了他的第一個專案,他翻了下蘋果的文件決定用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)
}
}
複製程式碼
到這裡,小樑覺得他功德圓滿,既重構了程式碼,又完成了產品的需求。
####但是,他真的功德圓滿嗎?