最终 class 的多态性实现了 swift 中的关联类型协议

Polymorphism with a final class that implements an associatedtype protocol in swift

我正在使用 Apollo v0.49.0。它是一个用于调用 graphQL 端点的库,其实现方式是在编译代码之前生成代码。

在说生成代码之前,先说一下生成代码实现了什么。对于这个问题,相关的是 GraphQLMutation。这是它的样子:

public enum GraphQLOperationType {
  case query
  case mutation
  case subscription
}

public protocol GraphQLOperation: AnyObject {
  var operationType: GraphQLOperationType { get }

  var operationDefinition: String { get }
  var operationIdentifier: String? { get }
  var operationName: String { get }

  var queryDocument: String { get }

  var variables: GraphQLMap? { get }

  associatedtype Data: GraphQLSelectionSet
}

public extension GraphQLOperation {
  var queryDocument: String {
    return operationDefinition
  }

  var operationIdentifier: String? {
    return nil
  }

  var variables: GraphQLMap? {
    return nil
  }
}

public protocol GraphQLQuery: GraphQLOperation {}
public extension GraphQLQuery {
  var operationType: GraphQLOperationType { return .query }
}

public protocol GraphQLMutation: GraphQLOperation {}
public extension GraphQLMutation {
  var operationType: GraphQLOperationType { return .mutation }
}

这是 the file 的 80%;最后 20% 是无关紧要的恕我直言。请注意 GraphQLMutation 如何实现 GraphQLOperation 而后者有一个 associatedtype.

该库根据您的 graphql 服务器端点生成 classes。它们的外观如下:

public final class ConcreteMutation: GraphQLMutation {
    ...
    public struct Data: GraphQLSelectionSet {
        ...
    }
    ...
}

据我所知(我是 Swift 的新手),我无法控制我目前提到的任何代码(除了分叉回购和修改它)。我可以在本地更改它们,但每次重新生成时它们都会被覆盖。

要使用这些生成的任何 classes,我必须将它们传递给这个 ApolloClient 函数(也是一个库 class):

@discardableResult
public func perform<Mutation: GraphQLMutation>(mutation: Mutation,
                                                 publishResultToStore: Bool = true,
                                                 queue: DispatchQueue = .main,
                                                 resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
    return self.networkTransport.send(
      operation: mutation,
      cachePolicy: publishResultToStore ? .default : .fetchIgnoringCacheCompletely,
      contextIdentifier: nil,
      callbackQueue: queue,
      completionHandler: { result in
        resultHandler?(result)
      }
    )
  }

我不知道如何以通用方式处理 ConcreteMutation。我希望能够像这样编写一个工厂函数:

extension SomeEnum {
   func getMutation<T: GraphQLMutation>() -> T {
        switch self {
            case .a:
                return ConcreteMutation1(first_name: value) as T
            case .b:
                return ConcreteMutation2(last_name: value) as T
            case .c:
                return ConcreteMutation3(bio: value) as T
            ...
        }
    }
}

这个函数在枚举中的事实与我无关:相同的代码可能在 struct/class/whatever 中。 重要的是函数签名。我想要一个 returns 一个 GraphQLMutation 可以传递给 ApolloClient.perform()

的工厂方法

因为我想不出一种方法来做这两件事,所以我最终写了一堆这样的函数:

func useConcreteMutation1(value) -> Void {
    let mutation = ConcreteMutation1(first_name: value)
    apolloClient.perform(mutation: mutation)
}

func useConcreteMutation2(value) -> Void {
    let mutation = ConcreteMutation2(last_name: value)
    apolloClient.perform(mutation: mutation)
}

...

这是很多重复的代码。

取决于我如何 fiddle 我的 getMutation 签名——例如,<T: GraphQLMutation>() -> T? 等——我可以得到要编译的函数,但我得到了不同的编译错误当我尝试将它传递给 ApolloClient.perform() 时。说“协议只能用作通用约束,因为它具有自我或相关类型要求。”

我对此进行了很多研究,我的研究发现 this article,但如果实现关联类型的具体 classes 是最终的,我认为这不是一个选项?

在这种情况下真的很难弄清楚是否可以使用多态性。我可以找到很多关于您可以 做什么的文章,但是找不到关于您不能 做什么的文章。我的问题是:

如何编写 getMutation 以便它 returns 一个可以传递给 ApolloClient.perform() 的值?

也许您需要在 associatedtype 上实现 AnyGraphQLMutation 类型擦除。 关于这件事(类型擦除),网上有很多资源,我发现 this one 非常详尽。

您 运行 遇到的根本问题是这个函数签名:

func getMutation<T: GraphQLMutation>() -> T

是模棱两可的。它不明确的原因是因为 GraphQLMutation 具有关联类型 (Data),并且该信息不会出现在函数声明中的任何位置。

当你这样做时:

extension SomeEnum {
   func getMutation<T: GraphQLMutation>() -> T {
        switch self {
            case .a:
                return ConcreteMutation1(first_name: value) as T
            case .b:
                return ConcreteMutation2(last_name: value) as T
            case .c:
                return ConcreteMutation3(bio: value) as T
            ...
        }
    }
}

这些分支中的每一个都可以有不同的类型。 ConcreteMutation1 可能有一个 DataDormouseConcreteMutation3 可能有一个 IceCreamTruck 的数据值。您也许可以告诉编译器忽略它,但是稍后您 运行 会遇到问题,因为 DormouseIceCreamTruck 是两个大小非常不同的结构,编译器可能需要使用不同的策略将它们作为参数传递。

Apollo.perform也是一个模板。编译器将基于该模板为您调用它的每种类型的突变编写不同的函数。为了做到这一点 必须 知道突变的完整类型签名,包括它的 Data 关联类型是什么。 responseHandler 回调应该能够处理 Dormouse 大小的东西,还是需要能够处理 IceCreamTruck 大小的东西?

如果编译器不知道,它就无法为 responseHandler 设置正确的调用顺序。如果您试图通过为 Dormouse!

大小的参数设计的回调调用序列来压缩 IceCreamTruck 大小的内容,就会发生不好的事情

如果编译器不知道变异必须提供什么类型的 Data,它就不能从模板中写出 perform 的正确版本。

如果您只将 func getMutation<T: GraphQLMutation>() -> T 的结果交给它,您已经消除了 Data 类型的证据,它不知道 perform 的版本它应该写。

您试图隐藏 Data 的类型,但您还希望编译器创建一个 perform 函数,其中 Data 的类型是已知的。你不能两者都做。

希望这对您有所帮助:

class GraphQLQueryHelper
{
    static let shared = GraphQLQueryHelper()

    class func performGraphQLQuery<T:GraphQLQuery>(query: T, completion:@escaping(GraphQLSelectionSet) -> ())
    {
        Network.shared.apollo().fetch(query: query, cachePolicy: .default) { (result) in
        
            switch result
            {
            case .success(let res):
                if let data = res.data
                {
                    completion(data)
                }
                else if let error = res.errors?.first
                {
                    if let dict = error["extensions"] as? NSDictionary
                    {
                        switch dict.value(forKey: "code") as? String ?? "" {
                        case "invalid-jwt": /*Handle Refresh Token Expired*/
                        default: /*Handle error*/
                            break
                        }
                    }
                    else
                    {
                        /*Handle error*/
                    }
                }
                else
                {
                    /*Handle Network error*/
                }
                break
            case .failure(let error):
                /*Handle Network error*/
                break
            }
        }
    }
    
    class func peroformGraphQLMutation<T:GraphQLMutation>(mutation: T, completion:@escaping(GraphQLSelectionSet) -> ())
    {
        Network.shared.apollo().perform(mutation: mutation) { (result) in
            switch result
            {
            case .success(let res):
                if let data = res.data
                {
                    completion(data)
                }
                else if let error = res.errors?.first
                {
                    if let dict = error["extensions"] as? NSDictionary
                    {
                        switch dict.value(forKey: "code") as? String ?? "" {
                        case "invalid-jwt": /*Handle Refresh Token Expired*/
                        default: /*Handle error*/
                            break
                        }
                    }
                    else
                    {
                        /*Handle error*/
                    }
                }
                else
                {
                   /*Handle Network error*/
                }
                break
            case .failure(let error):
                /*Handle error*/
                break
            }
        }
    }
}