简体   繁体   中英

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

I'm using Apollo v0.49.0 . It's a library for calling graphQL endpoints, and the way it does this is by generating code before you compile your code.

Before I talk about the generated code, I'd like to talk about what the generated code implements. For this question, it's the GraphQLMutation that's relevant. Here's what it looks like:

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 }
}

This is 80% of the file ; the last 20% is irrelevant IMHO. Note how GraphQLMutation implements GraphQLOperation and the latter has an associatedtype .

The library generates classes based on your graphql server endpoints. Here's what they look like:

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

As far as I know (I'm new to Swift), I have no control over any of the code I've mentioned so far (other than forking the repo and modifying it). I could change them locally, but they would just be overridden every time they were regenerated.

To use any of these generated classes, I have to pass them into this ApolloClient function (also a library 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)
      }
    )
  }

I can't figure out how to deal with ConcreteMutation in a generic way. I want to be able to write a factory function like so:

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
            ...
        }
    }
}

The fact that this func is in an enum is irrelevant to me: that same code could be in a struct/class/whatever. What matters is the function signature. I want a factory method that returns a GraphQLMutation that can be passed into ApolloClient.perform()

Because I can't figure out a way to do either of those things, I end up writing a bunch of functions like this instead:

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)
}

...

That's a lot of duplicated code.

Depending on how I fiddle with my getMutation signature -- eg, <T: GraphQLMutation>() -> T? etc. -- I can get the func to compile, but I get a different compile error when I try to pass it into ApolloClient.perform() . Something saying "protocol can only be used as a generic constraint because it has Self or associated type requirements."

I've researched this a lot, and my research found this article , but I don't think it's an option if the concrete classes implementing the associated type are final?

It's really difficult to figure out if it's possible to use polymorphism in this situation. I can find plenty of articles of what you can do, but no articles on what you can't do. My question is:

How do I write getMutation so it returns a value that can be passed into ApolloClient.perform() ?

The fundamental problem you are running into is that this function signature:

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

is ambiguous. The reason it's ambiguous is because GraphQLMutation has an associated type ( Data ) and that information doesn't appear anywhere in your function declaration.

When you do this:

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
            ...
        }
    }
}

Each of those branches could have a different type. ConcreteMutation1 could have a Data that is Dormouse while ConcreteMutation3 might have a data value that's an IceCreamTruck . You may be able too tell the compiler to ignore that but then you run into problems later because Dormouse and IceCreamTruck are two structs with VERY different sizes and the compiler might need to use different strategies to pass them as parameters.

Apollo.perform is also a template. The compiler is going to write a different function based on that template for each type of mutation you call it with. In order to do that must know the full type signature of the mutation including what its Data associated type is. Should the callback be able to handle something the size of a Dormouse , or does it need to be able to handle something the size of an IceCreamTruck ?

If the compiler doesn't know, it can't set up the proper calling sequence for the responseHandler . Bad things would happen if you tried to squeeze something the size of an IceCreamTruck through a callback calling sequence that was designed for a parameter the size of a Dormouse !

If the compiler doesn't know what type of Data the mutation has to offer, it can't write a correct version of perform from the template.

If you've only handed it the result of func getMutation<T: GraphQLMutation>() -> T where you've eliminated evidence of what the Data type is, it doesn't know what version of perform it should write.

You are trying to hide the type of Data , but you also want the compiler to create a perform function where the type of Data is known. You can't do both.

Maybe you need to implement AnyGraphQLMutation type erased over the associatedtype . There are a bunch of resources online for that matter (type erasure), I've found this one pretty exhaustive.

I hope this helps in someway:

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
            }
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM