簡體   English   中英

具有關聯值+泛型+具有關聯類型的協議的枚舉

[英]enums with Associated Values + generics + protocol with associatedtype

我正在嘗試使我的API服務盡可能通用:

API服務類別

class ApiService {
  func send<T>(request: RestRequest) -> T {
    return request.parse()
  }
}

這樣編譯器可以從請求類別.auth.data推斷響應類型:

let apiService = ApiService()

// String
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))
// Int
let intResponse = apiService.send(request: .data(.content(id: "123")))

我試圖提出一種使用泛型的解決方案以及具有關聯類型的協議來以一種干凈的方式處理解析。 但是我在以簡單且類型安全的方式將請求案例與不同的響應類型相關聯時遇到了麻煩:

protocol Parseable {
  associatedtype ResponseType
  func parse() -> ResponseType
}

端點

enum RestRequest {

  case auth(_ request: AuthRequest)
  case data(_ request: DataRequest)

  // COMPILER ERROR HERE: Generic parameter 'T' is not used in function signature
  func parse<T: Parseable>() -> T.ResponseType {
    switch self {
    case .auth(let request): return (request as T).parse()
    case .data(let request): return (request as T).parse()
    }
  }

  enum AuthRequest: Parseable {
    case login(email: String, password: String)
    case signupWithFacebook(token: String)

    typealias ResponseType = String
    func parse() -> ResponseType {
        return "String!!!"
    }
  }
  enum DataRequest: Parseable {
    case content(id: String?)
    case package(id: String?)

    typealias ResponseType = Int
    func parse() -> ResponseType {
        return 16
    }
  }
}

如何T不是即使我使用的函數簽名使用T.ResponseType作為函數的返回?

有沒有更好,更干凈的方法來實現這一目標?

我正在嘗試使我的API服務盡可能通用:

首先,也是最重要的是,這絕不應成為目標。 相反,您應該從用例開始,並確保您的API服務符合它們。 “盡可能通用”沒有任何意義,只會在您向事物添加“通用功能”時讓您陷入噩夢,這與通常在許多用例中有用的東西不同。 哪些呼叫者需要這種靈活性? 從呼叫者開始,然后將遵循協議。

func send<T>(request: RestRequest) -> T

接下來,這是一個非常糟糕的簽名。 您不希望對返回類型進行類型推斷。 這是一場噩夢。 相反,在Swift中執行此操作的標准方法是:

func send<ResultType>(request: RestRequest, returning: ResultType.type) -> ResultType

通過將預期結果類型作為參數傳遞,您可以擺脫類型推斷的麻煩。 頭痛看起來像這樣:

let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))

編譯器如何知道stringResponse應該是String? 這里什么也沒有說“字符串”。 因此,您必須這樣做:

let stringResponse: String = ...

這是非常丑陋的Swift。 相反,您可能想要(但不是真的):

let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")),
                                     returning: String.self)

“但不是真的”,因為沒有辦法很好地實現這一目標。 如何send知道如何將“我收到的任何響應”轉換為“碰巧稱為字符串的未知類型”的信息? 那會怎么辦?

protocol Parseable {
  associatedtype ResponseType
  func parse() -> ResponseType
}

這種PAT(帶有關聯類型的協議)實際上沒有任何意義。 它說如果某事物的實例可以返回ResponseType,則該事物是可解析的。 但這將是一個解析器,而不是“可以解析的東西”。

對於可以解析的內容,您需要一個可以接受一些輸入並自己創建的初始化。 通常,最好的方法是Codable,但是您可以自己制作,例如:

protocol Parseable {
    init(parsing data: Data) throws
}

但是我傾向於Codable,或者只是傳遞解析函數(見下文)。

enum RestRequest {}

這可能是枚舉的一種不好用法,尤其是如果您要尋找的是通用性。 每個新的RestRequest都需要更新parse ,這對於這種代碼來說是錯誤的地方。 枚舉使添加新的“所有實例實現的事物”變得容易,但添加“新種類的實例”卻很困難。 結構(+協議)則相反。 它們使添加新種類的協議變得容易,但是增加新協議需求卻變得困難。 后一種是請求,尤其是在通用系統中。 您想一直添加新請求。 枚舉使這一點變得困難。

有沒有更好,更干凈的方法來實現這一目標?

這取決於“這個”是什么。 您的調用代碼是什么樣的? 當前系統在哪里創建要消除的代碼重復? 您有哪些用例? 沒有“盡可能通用”這樣的東西。 僅有一些系統可以沿着用例准備好適應的軸適應用例。 不同的配置軸導致不同種類的多態性,並具有不同的權衡。

您希望您的調用代碼看起來像什么?

只是為了提供一個例子,看起來像這樣。

final class ApiService {
    let urlSession: URLSession
    init(urlSession: URLSession = .shared) {
        self.urlSession = urlSession
    }

    func send<Response: Decodable>(request: URLRequest,
                                   returning: Response.Type,
                                   completion: @escaping (Response?) -> Void) {
        urlSession.dataTask(with: request) { (data, response, error) in
            if let error = error {
                // Log your error
                completion(nil)
                return
            }

            if let data = data {
                let result = try? JSONDecoder().decode(Response.self, from: data)
                // Probably check for nil here and log an error
                completion(result)
                return
            }
            // Probably log an error
            completion(nil)
        }
    }
}

這是非常通用的,可以應用於多種用例(盡管這種特定形式非常原始)。 您可能會發現它不適用於所有用例,因此您將開始對其進行擴展。 例如,也許您不喜歡在這里使用Decodable。 您需要一個更通用的解析器。 很好,使解析器可配置:

func send<Response>(request: URLRequest,
                    returning: Response.Type,
                    parsedBy: @escaping (Data) -> Response?,
                    completion: @escaping (Response?) -> Void) {

    urlSession.dataTask(with: request) { (data, response, error) in
        if let error = error {
            // Log your error
            completion(nil)
            return
        }

        if let data = data {
            let result = parsedBy(data)
            // Probably check for nil here and log an error
            completion(result)
            return
        }
        // Probably log an error
        completion(nil)
    }
}

也許您想要這兩種方法。 很好,在另一個之上構建一個:

func send<Response: Decodable>(request: URLRequest,
                               returning: Response.Type,
                               completion: @escaping (Response?) -> Void) {
    send(request: request,
         returning: returning,
         parsedBy: { try? JSONDecoder().decode(Response.self, from: $0) },
         completion: completion)
}

如果您正在尋找更多有關此主題的信息,您可能會對“ Beyond Crusty”感興趣,其中包括一個將您正在討論的解析器捆綁在一起的可行示例。 有點過時了,Swift協議現在更強大了,但是基本消息沒有改變,並且在此示例中像parsedBy這樣的基礎。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM