许多年前,小梁进了他的第一家公司,不久迎来了他的第一个项目,他翻了下苹果的文档决定用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)
}
}
复制代码
到这里,小梁觉得他功德圆满,既重构了代码,又完成了产品的需求。
####但是,他真的功德圆满吗?