IOS/Swift: хороший подход к архитектуре для подключения API REST

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


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

Лучшие архитектурные подходы для создания сетевых приложений iOS (клиенты REST) ​​


Я не ищу ответов, как "использовать AFNetworking/Alamofire". Этот вопрос не зависит от того, какая сторонняя структура используется.

Я имею в виду, часто у нас есть сценарий:

"Разработайте приложение X, которое использует API Y"

И это включает в себя в основном те же самые шаги - каждый раз.

  • Внедрить логин/регистрацию
  • Вы получаете токен аутентификации, сохраните его в цепочке ключей и добавьте в каждый вызов API
  • Вам необходимо повторно аутентифицировать и повторно отправить запрос API, который не удалось выполнить с помощью 401
  • У вас есть коды ошибок для обработки (как обрабатывать их централизованно?)
  • Вы реализуете различные вызовы API.

Одна проблема с 3)

В Obj-C я использовал NSProxy для перехвата каждого вызова API перед его отправкой, повторной аутентификации пользователя, если токен истек, и уволил фактический запрос. В Swift у нас было несколько NSOperationQueue, где мы поставили в очередь вызов auth, если мы получили 401 и поставили в очередь фактический запрос после успешного обновления. Но это ограничило нас использованием Singleton (чего мне не очень нравится), и нам также пришлось ограничивать одновременные запросы до 1. Мне больше нравится второй подход - но есть ли лучшее решение?

Относительно 4)

Как вы обрабатываете коды статуса http? Вы используете много разных классов для каждой ошибки? Вы централизуете общую обработку ошибок в одном классе? Вы справляетесь с ними все на одном уровне или вы уловите ошибки сервера раньше? (Возможно, в вашем API-интерфейсе любой сторонней библиотеки)


Как разработчики пытаются решить эти проблемы? Вы поняли дизайн "лучшего совпадения"? Как вы тестируете свои API? Особенно, как вы это делаете в Swift (без реальной издевательской возможности?).

Конечно: каждый случай использования, каждое приложение, каждый сценарий различны - нет "Одно решение подходит для всех". Но я думаю, что эти общие проблемы повторяются так часто, поэтому я соблазнился сказать "Да, для этих случаев - может быть одно и несколько решений, которые вы можете использовать каждый раз".

С нетерпением ждем интересных ответов!

Приветствия
Орландо 🍻

Ответ 1

Но это ограничило нас использованием Singleton (чего мне не очень нравится), и нам также пришлось ограничить параллельные запросы на 1. Мне больше нравится второй подход - но есть ли лучшее решение?

Я использую несколько уровней для аутентификации с помощью API.

Менеджер проверки подлинности


Этот менеджер отвечает за все функции, связанные с проверкой подлинности. Вы можете подумать об аутентификации, reset password, повторить проверку кода кода и т.д.

struct AuthenticationManager
{
    static func authenticate(username:String!, password:String!) -> Promise<Void>
    {
        let request = TokenRequest(username: username, password: password)

        return TokenManager.requestToken(request: request)
    }
}

Чтобы запросить токен, нам нужен новый слой, называемый TokenManager, который управляет всеми вещами, связанными с .

Менеджер токенов


struct TokenManager
{
    private static var userDefaults = UserDefaults.standard
    private static var tokenKey = CONSTANTS.userDefaults.tokenKey
    static var date = Date()

    static var token:Token?
    {
        guard let tokenDict = userDefaults.dictionary(forKey: tokenKey) else { return nil }

        let token = Token.instance(dictionary: tokenDict as NSDictionary)

        return token
    }

    static var tokenExist: Bool { return token != nil }

    static var tokenIsValid: Bool
    {
        if let expiringDate = userDefaults.value(forKey: "EXPIRING_DATE") as? Date
        {
            if date >= expiringDate
            {
                return false
            }else{
                return true
            }
        }
        return true
    }

    static func requestToken(request: TokenRequest) -> Promise<Void>
    {
        return Promise { fulFill, reject in

            TokenService.requestToken(request: request).then { (token: Token) -> Void in
                setToken(token: token)

                let today = Date()
                let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)
                userDefaults.setValue(tomorrow, forKey: "EXPIRING_DATE")

                fulFill()
            }.catch { error in
                reject(error)
            }
        }
    }

    static func refreshToken() -> Promise<Void>
    {
        return Promise { fulFill, reject in

            guard let token = token else { return }

            let  request = TokenRefresh(refreshToken: token.refreshToken)

            TokenService.refreshToken(request: request).then { (token: Token) -> Void in
                setToken(token: token)
                fulFill()
            }.catch { error in
                reject(error)
            }
        }
    }

    private static func setToken (token:Token!)
    {
        userDefaults.setValue(token.toDictionary(), forKey: tokenKey)
    }

    static func deleteToken()
    {
        userDefaults.removeObject(forKey: tokenKey)
    }
}

Чтобы запросить токен, нам понадобится третий уровень под названием TokenService, который обрабатывает все HTTP-вызовы. Я использую EVReflection и Promises для вызовов API.

Служба токена


struct TokenService: NetworkService
{
    static func requestToken (request: TokenRequest) -> Promise<Token> { return POST(request: request) }

    static func refreshToken (request: TokenRefresh) -> Promise<Token> { return POST(request: request) }

    // MARK: - POST

    private static func POST<T:EVReflectable>(request: T) -> Promise<Token>
    {
        let headers = ["Content-Type": "application/x-www-form-urlencoded"]

        let parameters = request.toDictionary(.DefaultDeserialize) as! [String : AnyObject]

        return POST(URL: URLS.auth.token, parameters: parameters, headers: headers, encoding: URLEncoding.default)
    }
}

Служба авторизации


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

В Obj-C я использовал NSProxy для перехвата каждого вызова API перед его отправкой, повторной аутентификации пользователя, если токен истек, и уволил фактический запрос. В Swift у нас был некоторый NSOperationQueue, где мы поставили в очередь вызов auth, если получили 401 и поставили в очередь фактический запрос после успешного обновления. Но это ограничило нас использованием Singleton (чего мне не очень нравится), и нам также пришлось ограничить параллельные запросы на 1. Мне больше нравится второй подход - но есть ли лучшее решение?

struct AuthorizationService: NetworkService
{
    private static var authorizedHeader:[String: String]
    {
        guard let accessToken = TokenManager.token?.accessToken else
        {
            return ["Authorization": ""]
        }
        return ["Authorization": "Bearer \(accessToken)"]
    }

    // MARK: - POST

    static func POST<T:EVObject> (URL: String, parameters: [String: AnyObject], encoding: ParameterEncoding) -> Promise<T>
    {
        return firstly
        {
            return POST(URL: URL, parameters: parameters, headers: authorizedHeader, encoding: encoding)

        }.catch { error in

            switch ((error as NSError).code)
            {
            case 401:
                _ = TokenManager.refreshToken().then { return POST(URL: URL, parameters: parameters, encoding: encoding) }
            default: break
            }
        }
    }
}

Сетевая служба


Последняя часть будет . В этом слое сервиса мы сделаем все интерактивно-подобный код. Здесь все деловая логика, все, что связано с сетью. Если вы кратко просмотрите эту услугу, вы заметите, что здесь нет UI-логики, и это по какой-то причине.

protocol NetworkService
{
    static func POST<T:EVObject>(URL: String, parameters: [String: AnyObject]?, headers: [String: String]?, encoding: ParameterEncoding) -> Promise<T>

}

extension NetworkService
{
    // MARK: - POST

    static func POST<T:EVObject>(URL: String,
                                 parameters: [String: AnyObject]? = nil,
                                 headers: [String: String]? = nil, encoding: ParameterEncoding) -> Promise<T>
    {
        return Alamofire.request(URL,
                                 method: .post,
                                 parameters: parameters,
                                 encoding: encoding,
                                 headers: headers).responseObject()
    }
 }

Малая демонстрация аутентификации


Пример реализации этой архитектуры будет аутентификатором HTTP-запроса для входа пользователя в систему. Я покажу вам, как это делается с использованием архитектуры, описанной выше.

AuthenticationManager.authenticate(username: username, password: password).then { (result) -> Void in

// your logic

}.catch { (error) in

  // Handle errors

}

Обработка ошибок всегда является грязной задачей. У каждого разработчика есть собственный способ сделать это. В Интернете есть куча статей об обработке ошибок, например, в swift. Показ моей обработки ошибок будет не очень полезен, так как это мой личный способ сделать это, а также много кода для публикации в этом ответе, поэтому я скорее пропущу это.

В любом случае...

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

Это вопрос предпочтений, требований проекта и опыта в вашей команде.

Желаем удачи и, пожалуйста, не стесняйтесь обращаться ко мне, если есть какие-либо проблемы!