Swift - протокол может использоваться только как общее ограничение, потому что он имеет требования к типу Self или связанному типу.

Я работаю над приложением, которому необходимо запрашивать несколько API. Я придумал классы для каждого поставщика API (и, в более крайних случаях, класс для каждой конкретной конечной точки API). Это связано с тем, что каждый запрос API должен возвращать очень строгий тип ответа, поэтому, если API может, например, возвращать как профили пользователей, так и изображения профилей, я хочу, чтобы ответ был специфичным для любого из них.

Я реализовал это примерно следующим образом:

protocol MicroserviceProvider {
    associatedtype Response
}

protocol ProfilePictureMicroserviceProvider: MicroserviceProvider {
    func getPicture(by email: String, _ completion: (Response) -> Void)
}

class SomeProfilePictureAPI: ProfilePictureMicroserviceProvider {
    struct Response {
        let error: Error?
        let picture: UIImage?
    }

    func getPicture(by email: String, _ completion: (Response) -> Void) {
        // some HTTP magic 
        // will eventually call completion(_:) with a Response object 
        // which either holds an error or a UIImage.
    }
}

Поскольку я хочу иметь возможность использовать классы модульного тестирования, которые будут полагаться на этот API, мне нужно иметь возможность динамически вводить эту зависимость изображения профиля. По умолчанию он будет использовать SomeProfilePictureAPI, но при запуске тестов я смогу заменить его на MockProfilePictureAPI, который по-прежнему будет соответствовать ProfilePictureMicroserviceProvider.

И поскольку я использую связанные типы, мне нужно создавать классы, зависящие от ProfilePictureMicroserviceProvider generic.

Сначала я наивно пытался написать свой контроллер представления вот так

class SomeClass {
    var profilePicProvider: ProfilePictureMicroserviceProvider
}

Но это только привело к печально известному «Протокол ProfilePictureMicroserviceProvider, который можно использовать только как общее ограничение, потому что он имеет собственные или связанные требования типа», ошибка времени компиляции.

Теперь я читал об этой проблеме последние пару дней, пытаясь осмыслить протоколы со связанными типами (PATS), и решил, что выберу маршрут таких общих классов. :

class SomeClass<T: ProfilePictureMicroserviceProvider> {
    var profilePicProfider: T = SomeProfilePictureAPI() 
}

Но даже тогда я получаю следующую ошибку:

Cannot convert value of type 'SomeProfilePictureAPI' to specified type 'T'

Даже несмотря на то, что T ограничен протоколом ProfilePictureMicroserviceProvider и SomeProfilePictureAPI его придерживается ...

По сути, основная идея заключалась в достижении двух целей: обеспечить соблюдение структуры микросервиса с обязательным типом ответа и сделать каждый микросервис имитируемым для модульных тестов зависимых классов.

Сейчас я застрял на выборе одного из двух, так как не могу заставить его работать. Любая помощь в объяснении того, что я делаю не так, будет очень приветствоваться.

Я также посмотрел на стирание шрифта. Но мне это кажется очень странным и довольно сложным для чего-то, что выглядит не так во многих аспектах.

Итак, в основном мой вопрос состоит из двух частей: как я могу заставить свои микросервисы определять свой собственный тип ответа? И как я могу легко заменить их фиктивными микросервисами в классах, которые от них зависят?


person Skwiggs    schedule 20.06.2018    source источник
comment
Какой-то вопрос здесь, вы нашли обходной путь для этого?   -  person Tuan Do    schedule 05.02.2020
comment
@TuanDo Я сделал, и в конце концов написал из него Cocoapod / SPM, проверьте это: github.com/MrSkwiggs/ Netswift. Постараюсь на основании этого написать ответ   -  person Skwiggs    schedule 05.02.2020
comment
Спасибо. С нетерпением жду вашего ответа   -  person Tuan Do    schedule 07.02.2020
comment
@TuanDo Отправили ответ. Посмотрите и дайте мне знать, если вам что-нибудь понадобится :)   -  person Skwiggs    schedule 09.02.2020


Ответы (1)


Вы должны изменить эти требования;

Вместо того, чтобы внедрять MicroServiceProvider в каждый запрос, вы должны написать общий протокол MicroService «Connector», который должен определять, что он ожидает от каждого запроса и что каждый запрос ожидает от него.

Затем вы можете написать TestConnector, который соответствует этому протоколу, чтобы у вас был полный контроль над обработкой ваших запросов. Самое приятное то, что ваши запросы даже не нужно будет изменять.

Рассмотрим следующий пример:

protocol Request {
    // What type data you expect to decode and return
    associatedtype Response

    // Turn all the data defined by your concrete type 
    // into a URLRequest that we can natively send out.
    func makeURLRequest() -> URLRequest

    // Once the URLRequest returns, decode its content
    // if it succeeds, you have your actual response object 
    func decode(incomingData: Data?) -> Response?
}

protocol Connector {
    // Take in any type conforming to Request, 
    // do whatever is needed to get back some potential data, 
    // and eventually call the handler with the expected response
    func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void)
}

По сути, это минимальные требования для установки такой структуры. В реальной жизни вам потребуются дополнительные требования от вашего протокола запроса (например, способы определения URL-адреса, заголовки запроса, тело запроса и т. Д.).

Самое приятное то, что вы можете писать реализации по умолчанию для своих протоколов. Это избавляет от большого количества шаблонного кода! Итак, для настоящего коннектора вы можете сделать это:

extension Connector {
    func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void) {
        // Use a native URLSession
        let session = URLSession()

        // Get our URLRequest
        let urlRequest = request.makeURLRequest()

        // define how our URLRequest is handled
        let task = session.dataTask(with: urlRequest) { data, response, error in
            // Try to decode our expected response object from the request's data
            let responseObject = request.decode(incomingData: data)

            // send back our potential object to the caller's completion block
            handler(responseObject)
        }

        task.resume()
    }
}

Теперь все, что вам нужно сделать, это реализовать свой ProfilePictureRequest следующим образом (с дополнительными примерами переменных класса):

struct ProfilePictureRequest: Request {
    private let userID: String
    private let useAuthentication: Bool

    /// MARK: Conform to Request
    typealias Response = UIImage

    func makeURLRequest() -> URLRequest {
        // get the url from somewhere
        let url = YourEndpointProvider.profilePictureURL(byUserID: userID)

        // use that URL to instantiate a native URLRequest
        var urlRequest = URLRequest(url: url)

        // example use: Set the http method
        urlRequest.httpMethod = "GET"

        // example use: Modify headers
        if useAuthentication {
            urlRequest.setValue(someAuthenticationToken.rawValue, forHTTPHeaderField: "Authorization")
        }

        // Once the configuration is done, return the urlRequest
        return urlRequest
    }

    func decode(incomingData: Data?) -> Response? {
        // make sure we actually have some data
        guard let data = incomingData else { return nil }

        // use UIImage's native data initializer.
        return UIImage(data: data)
    }
}

Если затем вы хотите отправить запрос изображения профиля, все, что вам нужно сделать, это (вам понадобится конкретный тип, который соответствует коннектору, но поскольку протокол коннектора имеет реализации по умолчанию, этот конкретный тип в этом примере в основном пуст. : struct GenericConnector: Connector {}):

// Create an instance of your request with the arguments you desire
let request = ProfilePictureRequest(userID: "JohnDoe", useAuthentication: false)

// perform your request with the desired Connector
GenericConnector().perform(request) { image in 
    guard let image = image else { return }

    // You have your image, you can now use that instance whichever way you'd like
    ProfilePictureViewController.current.update(with: image)
}

И, наконец, чтобы настроить TestConnector, все, что вам нужно сделать, это:

struct TestConnector: Connector {

    // define a convenience action for your tests
    enum Behavior {
        // The network call always fails
        case alwaysFail

        // The network call always succeeds with the given response
        case alwaysSucceed(Any)
    }

    // configure this before each request you want to test
    static var behavior: Behavior

    func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void) {
        // since this is a test, you don't need to actually perform any network calls.
        // just check what should be done
        switch Self.behavior {
        case alwaysFail:
            handler(nil)

        case alwaysSucceed(let response):
            handler(response as! T)
        }
    }
}

С его помощью вы можете легко определять запросы, как они должны настраивать свои действия с URL-адресами и как они декодируют свой собственный тип ответа, а также вы можете легко писать макеты для своих коннекторов.

Конечно, имейте в виду, что примеры, приведенные в этом ответе, довольно ограничены в том, как их можно использовать. Я настоятельно рекомендую вам взглянуть на эту библиотеку, которую я написал. Он расширяет этот пример гораздо более структурированным образом.

person Skwiggs    schedule 09.02.2020
comment
Большое спасибо за подробное объяснение. Это очень легко понять. Продолжайте в том же духе (у). - person Tuan Do; 10.02.2020