具有关联值的枚举 + 泛型 + 具有关联类型的协议

enums with Associated Values + generics + protocol with associatedtype

我正在努力使我的 API 服务尽可能通用:

API 服务 Class

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.ResponseType 用作函数 return,为什么 T 不用于函数签名?

有没有更干净的方法来实现这个目标?

I'm trying to make my API Service as generic as possible:

首先,也是最重要的一点,这绝不应该成为目标。相反,您应该从用例开始,并确保您的 API 服务满足它们。 "As generic as possible" 没有任何意义,只会让您在向事物添加 "generic features" 时陷入类型噩梦,这与对许多用例通常有用的东西不同。哪些 呼叫者 需要这种灵活性?从呼叫者开始,协议将随之而来。

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

接下来,这是一个非常糟糕的签名。您不希望对 return 类型进行类型推断。这是一场噩梦。相反,在 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)

"But not really" 因为没有办法很好地实现这个。 send 怎么知道如何将 "whatever response I get" 翻译成 "an unknown type that happens to be called String?" 那会做什么?

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

这个 PAT(带有关联类型的协议)没有任何意义。它说如果它的一个实例可以 return 一个 ResponseType,那么它是可解析的。但那将是一个 parser 而不是 "something that can be parsed."

对于可以解析的东西,您需要一个可以接受一些输入并自行创建的 init。最好的通常是 Codable,但你可以自己制作,例如:

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

但我更倾向于 Codable,或者只是传递解析函数(见下文)。

enum RestRequest {}

这可能是对枚举的错误使用,尤其是当您要寻找的是一般可用性时。每个新的 RestRequest 都需要更新 parse,这是此类代码的错误位置。枚举使添加新的 "things that all instances implement" 变得容易,但很难添加 "new kinds of instances." 结构(+协议)则相反。它们使添加新的 协议变得容易,但很难添加新的协议要求。请求,尤其是在通用系统中,属于后一种。您想一直添加新请求。枚举使这变得困难。

Is there a better still clean way to achieve this?

这取决于 "this" 是什么。您的调用代码是什么样的?您当前的系统在哪里创建您想要消除的代码重复?你的用例是什么?没有 "as generic as possible." 这样的东西,只有系统可以沿着它们准备处理的轴适应用例。不同的配置轴导致不同种类的多态性,并有不同的权衡。

您希望调用代码是什么样的?

只是为了提供一个示例,说明它可能是什么样子,但应该是这样的。

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: [=20=]) },
         completion: completion)
}

如果您正在寻找有关此主题的更多信息,您可能会对 "Beyond Crusty" 感兴趣,其中包括一个将您正在讨论的类型的解析器捆绑在一起的解决方案。它有点过时了,Swift 协议现在更强大了,但是基本消息并没有改变,并且在这个例子中像 parsedBy 这样的东西的基础。