Я пытаюсь сделать мой 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
в этом примере.