前言
class Light {
func 插電() {}
func 開啟() {}
func 增大亮度() {}
func 減小亮度() {}
}
class LEDLight: Light {}
class DeskLamp: Light {}
func 開啟(物體: Light) {
物體.插電()
物體.開啟()
}
func main() {
開啟(物體: DeskLamp())
開啟(物體: LEDLight())
}
複製程式碼
在上述物件導向的實現中開啟
方法似乎只侷限於Light
這個類和他的派生類。如果我們想描述開啟
這個操作並且不單單侷限於Light
這個類和他的派生類,(畢竟櫃子、桌子等其他物體也是可以開啟的)抽象開啟
這個操作,那麼protocol
就可以派上用場了。
protocol Openable {
func 準備工作()
func 開啟()
}
extension Openable {
func 準備工作() {}
func 開啟() {}
}
class LEDLight: Openable {}
class DeskLamp: Openable {}
class Desk: Openable {}
func 開啟<T: Openable>(物體: T) {
物體.準備工作()
物體.開啟()
}
func main() {
開啟(物體: Desk())
開啟(物體: LEDLight())
}
複製程式碼
普通的網路請求
// 1.準備請求體
let urlString = "https://www.baidu.com/user"
guard let url = URL(string: urlString) else {
return
}
let body = prepareBody()
let headers = ["token": "thisisatesttokenvalue"]
var request = URLRequest(url: url)
request.httpBody = body
request.allHTTPHeaderFields = headers
request.httpMethod = "GET"
// 2.使用URLSeesion建立網路任務
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let data = data {
// 3.將資料反序列化
}
}.resume()
複製程式碼
我們可以看到發起一個網路請求一般會有三個步驟
- 準備請求體(URL、parameters、body、headers...)
- 使用框架建立網路任務(URLSession、Alamofire、AFN...)
- 將資料反序列化(Codable、Protobuf、SwiftyJSON、YYModel...)
我們可以把這三個步驟進行抽象,用三個protocol
進行規範.
規範好之後,再由各個型別實現這三個協議,就可以隨意組合使用.
抽象網路請求步驟
Parsable
首先我們定義Parsable
協議來抽象反序列化這個過程
protocol Parsable {
// ps: Result型別下邊會宣告,這裡姑且可以認為函式返回了`Self`
static func parse(data: Data) -> Result<Self>
}
複製程式碼
Parsable
協議定義了一個靜態方法,這個方法可以從Data -> Self
例如User
遵循Parsable
協議,就要實現從Data轉換到User的parse(:)
方法
struct User {
var name: String
}
extension User: Parsable {
static func parse(data: Data) -> Result<User> {
// ...實現Data轉User
}
}
複製程式碼
Codable
我們可以利用swift協議擴充套件
的特性給遵循Codable
的型別新增一個預設的實現
extension Parsable where Self: Decodable {
static func parse(data: Data) -> Result<Self> {
do {
let model = try decoder.decode(self, from: data)
return .success(model)
} catch let error {
return .failure(error)
}
}
}
複製程式碼
這樣User
如果遵循了Codable
,就無需實現parse(:)
方法了
於是反序列化的過程就變這樣簡單的一句話
extension User: Codable, Parsable {}
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let data = data {
// 3.將資料反序列化
let user = User.parse(data: data)
}
複製程式碼
到這裡可以想一個問題,如果data是個模型陣列該怎麼辦?是不是在Parsable
協議裡再新增一個方法返回一個模型陣列?然後再實現一遍?
public protocol Parsable {
static func parse(data: Data) -> Result<Self>
// 返回一個陣列
static func parse(data: Data) -> Result<[Self]>
}
複製程式碼
這樣也不是不行,但是還有更swift的方法,這種方法swift稱之為條件遵循
// 當Array裡的元素遵循Parsable以及Decodable時,Array也遵循Parsable協議
extension Array: Parsable where Array.Element: (Parsable & Decodable) {}
複製程式碼
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let data = data {
// 3.將資料反序列化
let users = [User].parse(data: data)
}
複製程式碼
從這裡可以看到swift協議是非常強大的,使用好了可以減少很多重複程式碼,在swift標準庫中有很多這樣的例子。
protobuf
當然,如果你使用SwiftProtobuf
,也可以提供它的預設實現
extension Parsable where Self: SwiftProtobuf.Message {
static func parse(data: Data) -> Result<Self> {
do {
let model = try self.init(serializedData: data)
return .success(model)
} catch let error {
return .failure(error)
}
}
}
複製程式碼
反序列化的過程也和剛才的例子一樣,呼叫parse(:)
方法即可
Request
現在我們定義Request
協議來抽象準備請求體這個過程
protocol Request {
var url: String { get }
var method: HTTPMethod { get }
var parameters: [String: Any]? { get }
var headers: HTTPHeaders? { get }
var httpBody: Data? { get }
/// 請求返回型別(需遵循Parsable協議)
associatedtype Response: Parsable
}
複製程式碼
我們定義了一個關聯型別:遵循Parsable
的 Response
是為了讓實現這個協議的型別指定這個請求返回的型別,限定Response
必須遵循Parsable
是因為,我們會用到parse(:)
方法來進行反序列化。
我們來實現一個通用的請求體
struct NormalRequest<T: Parsable>: Request {
var url: String
var method: HTTPMethod
var parameters: [String: Any]?
var headers: HTTPHeaders?
var httpBody: Data?
typealias Response = T
init(_ responseType: Response.Type,
urlString: String,
method: HTTPMethod = .get,
parameters: [String: Any]? = nil,
headers: HTTPHeaders? = nil,
httpBody: Data? = nil) {
self.url = urlString
self.method = method
self.parameters = parameters
self.headers = headers
self.httpBody = httpBody
}
}
複製程式碼
是這樣使用的
let request = NormalRequest(User.self, urlString: "https://www.baidu.com/user")
複製程式碼
如果服務端有一組介面
https://www.baidu.com/user
https://www.baidu.com/manager
https://www.baidu.com/driver
我們可以定義一個BaiduRequest
,把URL或者公共的headers和body拿到BaiduRequest
管理
// BaiduRequest.swift
private let host = "https://www.baidu.com"
enum BaiduPath: String {
case user = "/user"
case manager = "/manager"
case driver = "/driver"
}
struct BaiduRequest<T: Parsable>: Request {
var url: String
var method: HTTPMethod
var parameters: [String: Any]?
var headers: HTTPHeaders?
var httpBody: Data?
typealias Response = T
init(_ responseType: Response.Type,
path: BaiduPath,
method: HTTPMethod = .get,
parameters: [String: Any]? = nil,
headers: HTTPHeaders? = nil,
httpBody: Data? = nil) {
self.url = host + path.rawValue
self.method = method
self.parameters = parameters
self.httpBody = httpBody
self.headers = headers
}
}
複製程式碼
建立也很簡單
let userRequest = BaiduRequest(User.self, path: .user)
let managerRequest = BaiduRequest(Manager.self, path: .manager, method: .post)
複製程式碼
Client
最後我們定義Client
協議,抽象發起網路請求的過程
enum Result<T> {
case success(T)
case failure(Error)
}
typealias Handler<T> = (Result<T>) -> ()
protocol Client {
// 接受一個遵循Parsable的T,最後回撥閉包的引數是T裡邊的Response 也就是Request協議定義的Response
func send<T: Request>(request: T, completionHandler: @escaping Handler<T.Response>)
}
複製程式碼
URLSession
我們來實現一個使用URLSession
的Client
struct URLSessionClient: Client {
static let shared = URLSessionClient()
private init() {}
func send<T: Request>(request: T, completionHandler: @escaping (Result<T.Response>) -> ()) {
var urlString = request.url
if let param = request.parameters {
var i = 0
param.forEach {
urlString += i == 0 ? "?\($0.key)=\($0.value)" : "&\($0.key)=\($0.value)"
i += 1
}
}
guard let url = URL(string: urlString) else {
return
}
var req = URLRequest(url: url)
req.httpMethod = request.method.rawValue
req.httpBody = request.httpBody
req.allHTTPHeaderFields = request.headers
URLSession.shared.dataTask(with: req) { (data, respose, error) in
if let data = data {
// 使用parse方法反序列化
let result = T.Response.parse(data: data)
switch result {
case .success(let model):
completionHandler(.success(model))
case .failure(let error):
completionHandler(.failure(error))
}
} else {
completionHandler(.failure(error!))
}
}
}
}
複製程式碼
三個協議實現好之後 例子開頭的網路請求就可以這樣寫了
let request = NormalRequest(User.self, urlString: "https://www.baidu.com/user")
URLSessionClient.shared.send(request) { (result) in
switch result {
case .success(let user):
// 此時拿到的已經是User例項了
print("user: \(user)")
case .failure(let error):
printLog("get user failure: \(error)")
}
}
複製程式碼
Alamofire
當然也可以用Alamofire
實現Client
struct NetworkClient: Client {
static let shared = NetworkClient()
func send<T: Request>(request: T, completionHandler: @escaping Handler<T.Response>) {
let method = Alamofire.HTTPMethod(rawValue: request.method.rawValue) ?? .get
var dataRequest: Alamofire.DataRequest
if let body = request.httpBody {
var urlString = request.url
if let param = request.parameters {
var i = 0
param.forEach {
urlString += i == 0 ? "?\($0.key)=\($0.value)" : "&\($0.key)=\($0.value)"
i += 1
}
}
guard let url = URL(string: urlString) else {
print("URL格式錯誤")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
urlRequest.httpBody = body
urlRequest.allHTTPHeaderFields = request.headers
dataRequest = Alamofire.request(urlRequest)
} else {
dataRequest = Alamofire.request(request.url,
method: method,
parameters: request.parameters,
headers: request.headers)
}
dataRequest.responseData { (response) in
switch response.result {
case .success(let data):
// 使用parse(:)方法反序列化
let parseResult = T.Response.parse(data: data)
switch parseResult {
case .success(let model):
completionHandler(.success(model))
case .failure(let error):
completionHandler(.failure(error))
}
case .failure(let error):
completionHandler(.failure(error))
}
}
}
private init() {}
}
複製程式碼
我們試著發起一組網路請求
let userRequest = BaiduRequest(User.self, path: .user)
let managerRequest = BaiduRequest(Manager.self, path: .manager, method: .post)
NetworkClient.shared.send(managerRequest) { result in
switch result {
case .success(let manager):
// 此時拿到的已經是Manager例項了
print("manager: \(manager)")
case .failure(let error):
printLog("get manager failure: \(error)")
}
}
複製程式碼
總結
我們用三個protocol
抽象了網路請求的過程,讓網路請求變得很靈活,你可以隨意組合各種實現,不同的請求體配不同的序列化方式或者不同的網路框架。可以使用URLSession + Codable,也可以使用Alamofire + Protobuf等等,極大的方便了我們日常開發。
引用
喵神的這篇文章是我學習面向協議的開始,給了我極大的啟發:面向協議程式設計與 Cocoa 的邂逅