перечисления с ассоциированными значениями + обобщенные элементы + протокол с ассоциированным типом - PullRequest
2 голосов
/ 14 марта 2019

Я пытаюсь сделать свою службу 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 в качестве возврата функции?

Есть ли еще лучший способ добиться этого?

1 Ответ

4 голосов
/ 14 марта 2019

Я пытаюсь сделать мой 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 должен быть строкой?Ничто здесь не говорит "Строка".Поэтому вместо этого вы должны сделать следующее:

let stringResponse: String = ...

И это очень уродливо, Свифт.Вместо этого вы, вероятно, хотите (но не совсем):

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

«Но не совсем», потому что нет способа реализовать это хорошо.Как send может знать, как перевести "любой ответ, который я получаю" в "неизвестный тип, который называется String?"Что бы это делало?

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

Этот PAT (протокол с ассоциированным типом) на самом деле не имеет смысла.Он говорит, что что-то можно разобрать, если его экземпляр может вернуть ResponseType.Но это будет синтаксический анализатор , а не "что-то, что может быть проанализировано".

Для чего-то, что может быть проанализировано, вы хотите, чтобы инициализация могла принимать некоторый ввод и создавать себя.Лучшим для этого обычно является Codable, но вы можете сделать свой собственный, например:

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

Но я бы склонялся к Codable или просто передавал функцию синтаксического анализа (см. Ниже).

enum RestRequest {}

Это, вероятно, неправильное использование enum, особенно если вы ищете общее удобство использования.Каждый новый запрос 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 в этом примере.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...