Cетевой слой в iOS-приложении

    Практически любое мобильное приложение взаимодействует с серверами через их API. Перед разработчиком в таком случае стоит задача реализовать сетевой слой своего приложения. Провайдеры того или иного API разрабатывают его интерфейс, зачастую, одинаково, но бывает и так, что API имеет свою специфику. Например, API Вконтакте при какой-либо ошибке в обращении к их методам не отображает это в статус коде ответа, а отображает это в самом теле ответа как JSON по ключу «error»: то есть, во-первых, вы не поймете по статус коду прошел ли запрос удачно, а во-вторых, не узнаете, какая произошла ошибка пока не измените логику обработки ответа. Таким образом, перед разработчиком лежит задача реализации достаточно гибкого слоя, контроль над которым можно осуществлять на разных этапах работы с сервером.

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

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

    Вот как это будет выглядеть в итоге:

    import UIKit
    
    class ViewController: UIViewController {
    
        let service: WallPostable = BasicWallAPI()
        
        @IBOutlet weak var textField: UITextField!
    
        @IBAction func postAction() {
            service.postWall(with: textField.text!)
        }
    }
    

    Итак, как выглядит работа с API для конечного пользователя (я имею в виду программиста, который использует реализацию слоя):

    image

    Немного раскроем ящик API:

    image

    Как я вижу ящик Send:

    image

    Мы формируем запрос. Обычно запросы имеют некоторые одинаковые хэдеры и чтобы не прописывать их в каждом запросе, мы подготавливаем запросы в блоке «Request Preparation». Далее полноценно собранный запрос мы отправляем, используя удобный для нас фреймворк (В моем примере я использую нативный фреймворк от Apple). Отправить запрос всегда самое простое, все сложное начинается при получении ответа.

    Как я вижу ящик Handle:

    image

    Пришедший ответ мы проверяем на успешность выполнения, далее в случае успешного ответа мы преобразуем его в модель понятную для приложения и отдаем. Если запрос выполнился не успешно, то мы отдаем его в блок обработки ошибки: он достает код ошибки, сообщение если оно имеется, реагирует на все это как мы ему говорим, формирует ошибку понятную для приложения и отдает.

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

    Итак, что нам нужно:

    • Запрос

      enum HTTPMethod: String {
          case GET
          case POST
          case PUT
          case DELETE
      }
      
      protocol HTTPRequestRepresentable {
          var path: String { get set }
          var httpMethod: HTTPMethod { get }
          var parameters: JSON? { get set }
          var headerFields: [String: String]? { get set }
          var bodyString: String? { get set }
      }
      
      extension HTTPRequestRepresentable {
          func urlRequest() -> URLRequest? {
              guard var urlComponents = URLComponents(string: self.path) else {
                  return nil
              }
              
              if let parametersJSON = self.parameters {
                  var queryItems = [URLQueryItem]()
                  for (key, value) in parametersJSON {
                      queryItems.append(URLQueryItem(name: key, value: value as? String))
                  }
                  urlComponents.queryItems = queryItems
              }
              
              guard let url = urlComponents.url else {
                  return nil
              }
              
              var urlRequest = URLRequest(url: url)
              urlRequest.httpMethod = self.httpMethod.rawValue
              urlRequest.allHTTPHeaderFields = headerFields
              if let body = bodyString {
                  urlRequest.httpBody = body.data(using: .utf8)
              }
              
              return urlRequest
          }
      }
      
    • Отправитель запроса, который будет описан позже
    • Обработчик ответа

      
      enum Result<T, E> {
          case Value(T)
          case Error(E)
      }
      
      protocol ResponseHandler {
          associatedtype ResultType
          associatedtype ErrorType
          
          func handleResponse(_ response: ResponseRepresentable, completion: (Result<ResultType, ErrorType>) -> ())
      }
      
    • Модель, в которую мы хотим преобразовать ответ
      Этот слой построен с учетом прекрасной возможности в Swift 4, которая предоставила нам такой протокол как Decodable, при реализации которого ваша модель будет самостоятельно (почти) создана из пришедшего ответа.

    Отправитель запроса нуждается в:

    • Подготовителе запроса

      protocol RequestPreparator {
          func prepareRequest(_ request: inout HTTPRequestRepresentable)
      }
      

    Обработчик ответа в свою очередь нуждается в:

    • Валидаторе ответа (проверяет ответ на успешность)
      protocol SuccessResponseChecker {
          func isSuccessResponse(_ response: ResponseRepresentable) -> Bool
      }
      
    • Обработчике неудачного ответа

      protocol ErrorHandler {
          var errorCodeHandler: ErrorCodeHandler { get set }
          
          func handleError(_ error: ErrorRepresentable)
      }
      
    • Преобразователе ответа в модель

      protocol DecodingProcessor {
          associatedtype DecodingResult
          
          func decodeFrom(_ data: Data) throws -> DecodingResult
      }
      


    Теперь об отправителе запроса. Им в нашем случае будет являться сервис, которому передается запрос, на нем лежит обязанность отправить его и затем передать ответ обработчику ответа.

    protocol Service {
        associatedtype ResultType: Decodable
        associatedtype ErrorType: ErrorRepresentable
        
        typealias SuccessHandlerBlock = (ResultType) -> ()
        typealias FailureHandlerBlock = (ErrorType) -> ()
        
        var request: HTTPRequestRepresentable? { get set }
        var responseHandler: HTTPResponseHandler<ResultType, ErrorType>? { get set }
      
        func sendRequest() -> Self?
    }
    

    Себя мы будем возвращать для реализации Method Chaining'а.

    Как видите я только описал протоколы, по которым вся эта система будет общаться с объектами внутри системы. Это дает нам возможность контролировать любой нужный нам этап, просто инжектировав свою реализацию протокола.

    Нередко бывает так, что для более чем одного запроса при ошибке приходит один и тот же код, но означает он для них совершенно разное. Когда сервис — один большой объект, который все сам и обрабатывает, возникают проблемы, ведущие к «костылям» и неизбежному росту класса и уменьшению его красоты. В данном случае, если вам надо как-то по своему отреагировать на ошибку, вы просто реализуете протокол обработчика ошибки и инжектируете его в обработчика ответа: ничего менять в других местах вы не будете, вы расширите систему, а не модифицируете ее, что есть хороший пример Open-Close принципа с точки зрения системы. Все объекты выполняют одну свою роль: Single Responsibility Principle. Выполнение остальных принципов, думаю очевидно.

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

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

    Итак, вот как выглядит наш сервис, отправляющий запрос:

    final class BaseService<T: Decodable, E: ErrorRepresentable>: Service {
        typealias ResultType = T
        typealias ErrorType = E
        
        var responseHandler: HTTPResponseHandler<T, E>? = HTTPResponseHandler<T, E>()
        var request: HTTPRequestRepresentable?
    
        var successHandler: SuccessHandlerBlock?
        var failureHandler: FailureHandlerBlock?
        var noneHandler: (() -> ())?
        
        var requestPreparator: RequestPreparator? = BaseRequestPreparator()
        
        private var session: URLSession {
            let session = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: nil)
            
            return session
        }
        
        @discardableResult
        func sendRequest() -> BaseService<T, E>? {
            guard var request = request else {
                return nil
            }
            
            requestPreparator?.prepareRequest(&request)
            
            guard let urlRequest = request.urlRequest() else {
                return nil
            }
    
            session.dataTask(with: urlRequest) { [weak self] (data, response, error) in
                let response = BaseResponse(data: data, response: response, error: error)
                
                self?.responseHandler?.handleResponse(response, completion: { [weak self] (result) in
                    switch result {
                    case let .Value(model):
                        self?.processSuccess(model)
                    case let .Error(error):
                        self?.processError(error)
                    }
                })
            }.resume()
            
            return self
        }
        
        @discardableResult
        func onSucces(_ success: @escaping SuccessHandlerBlock) -> BaseService<T, E> {
            successHandler = success
            
            return self
        }
        
        @discardableResult
        func onFailure(_ failure: @escaping FailureHandlerBlock) -> BaseService<T, E> {
            failureHandler = failure
            
            return self
        }
        
        private func processSuccess(_ model: T) {
            successHandler?(model)
            successHandler = nil
        }
    
        private func processError(_ error: E) {
            failureHandler?(error)
            failureHandler = nil
        }
    }
    

    Как видно, все что он делает это подготовка запроса, отправка его с помощью Apple'вского фреймворка и передача ответа на обработку обработчику ответов.

    Как вы поняли, параметр обобщенного класса T — это тип итоговой модели, а E — тип ошибки. Знание этих типов в большей степени нужно обработчику ответа, взглянем на него:

    class HTTPResponseHandler<T: Decodable, E: ErrorRepresentable>: ResponseHandler {
        typealias ResultType = T
        typealias ErrorType = E
        
        private var isResponseRepresentSimpleType: Bool {
            return
                T.self == Int.self ||
                T.self == String.self ||
                T.self == Double.self ||
                T.self == Float.self
        }
        
        var errorHandler: ErrorHandler = BaseErrorHandler()
        var successResponseChecker: SuccessResponseChecker = BaseSuccessResponseChecker()
        var decodingProcessor = ModelDecodingProcessor<T>()
        var nestedModelGetter: NestedModelGetter?
        
        func handleResponse(_ response: ResponseRepresentable, completion: (Result<T, E>) -> ()) {
            if successResponseChecker.isSuccessResponse(response) {
                processSuccessResponse(response, completion: completion)
            } else {
                processFailureResponse(response, completion: completion)
            }
        }
        
        private func processSuccessResponse(_ response: ResponseRepresentable, completion: (Result<T, E>) -> ()) {
            guard var data = response.data else {
                return
            }
           
           //Часть кода удалена при копировании в статью, чтобы не отвлекать от основной реализации
    
            guard let result = try? decodingProcessor.decodeFrom(data) else {
                completion(Result.Error(E(ProcessingErrorType.modelProcessingError)))
                
                return
            }
            
            completion(.Value(result))
        }
        
        private func simpleTypeUsingNestedModelGetter(from data: Data) -> T? {
            let getter = nestedModelGetter!
            
            guard let escapedModelJSON = try? getter.getFrom(data) else {
                return nil
            }
            
            guard let result = escapedModelJSON[getter.escapedModelKey] as? T else {
                return nil
            }
            
            return result
        }
        
        
        private func processFailureResponse(_ response: ResponseRepresentable, completion: (Result<T, E>) -> ()) {
            let error = E(response)
            completion(.Error(error))
            errorHandler.handleError(error)
        }
        
    }
    

    Я специально удалил некоторые строки, чтобы не отвлекать от основной реализации. Что делают эти удаленные строки? Я реализовал возможность извлечения вложенной модели, если остально в ответе вам не нужно. Такое бывает, например в случае, когда вы запрашиваете стенку ВК и он возвращает что-то вроде:

    ["response": {
    "items": [{"id":1, "text": "some"}, {"id":2, "text": "awesome"}],
    "count": 231
    }]
    

    а вам нужны только items, в таком случае вы можете дать обработчику ответа NestedModelGetter, который имеет проперти keyPath в виде «response.items», и он вытащит для вас вашу модель. Также в моей реализации можно достать count, то есть примитивный тип, для которого вы конечно должны дать объект, который data в этот тип преобразует.

    Весь проект доступен ниже по ссылке, там вы все можете увидеть.

    Также видно, что у обработчика ответа есть валидатор успешности и в случае неуспешного ответа мы отдаем ответ обработчику ошибок.

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

    struct WallItem: Decodable {
        var id: Int
        var text: String
    }
    

    Модели просто реализуют протокол Decodable, и, конечно, если ключи в JSON не совпадают с именами у модели, необходимо также добавить в модель CodingKeys. Об этом всем можно почитать в документации Apple.

    Также приведу пример валидатора успешного запроса для API VK:

    struct VKAPISuccessChecker: SuccessResponseChecker {
        let jsonSerializer = JSONSerializer()
        
        func isSuccessResponse(_ response: ResponseRepresentable) -> Bool {
            guard let httpResponse = response.response as? HTTPURLResponse else {
                return false
            }
            
            let isSuccesAccordingToStatusCode = Range(uncheckedBounds: (200, 300)).contains(httpResponse.statusCode)
            
            guard let data = response.data else {
                return false
            }
            
            guard let json = try? jsonSerializer.serialize(data) else {
                return false
            }
            
            return isSuccesAccordingToStatusCode && !json.keys.contains("error")
        }
    }
    

    Процесс сборки сервиса для запроса стенки ВК выглядит так:

    let service = BaseService<Wall, VKAPIError>()
    service.request = GETWallRequest()
            
    let responseHandler = HTTPResponseHandler<Wall, VKAPIError>()
    responseHandler.nestedModelGetter = ResponseModelGetter.wallResponse
    responseHandler.successResponseChecker = VKAPISuccessChecker()
    
    service.responseHandler = responseHandler
            
    return service
    


    Немного об ошибках. Все типы ошибок реализуют протокол:
    protocol ErrorRepresentable {
        var message: String? { get set }
        var errorCode: Int? { get set }
        var type: ErrorType { get set }
        
        init(_ type: ErrorType)
        init(_ response: ResponseRepresentable)
    }
    
    protocol ErrorType {
        var rawValue: String { get }
    }
    


    ErrorType реализуют у меня enum'ы. Взглянем на структуры ошибки ВК:

    enum VKAPIErrorType: String, ErrorType {
        case invalidAccessToken
        case unknownError
    }
    
    struct VKAPIError: ErrorRepresentable {
        var errorCode: Int?
        var message: String?
        var type: ErrorType = VKAPIErrorType.unknownError
        
        init(_ type: ErrorType) {
            self.type = type
        }
        
        init(_ response: ResponseRepresentable) {
            guard let data = response.data else {
                return
            }
            
            let jsonSerializer = JSONSerializer()
            guard let dataJSON = try? jsonSerializer.serialize(data),
                let errorJSON = dataJSON["error"] as? JSON else {
                return
            }
    
            errorCode = errorJSON["error_code"] as? Int
            message = errorJSON["error_msg"] as? String
            
            guard let code = errorCode else {
                return
            }
            
            switch code {
            case 5:
                type = VKAPIErrorType.invalidAccessToken
            default:
                type = VKAPIErrorType.unknownError
            }
        }
    }
    


    Думаю, никакому адекватному разработчику не хотелось бы прописывать сборку сервиса в каждом контроллере. Мне тоже, поэтому завернем процесс сборки в Builder:

    protocol APIBuilder {
        associatedtype ErrorType: ErrorRepresentable
        
        func buildAPI<T: Decodable>(_ responseType: T.Type, request: HTTPRequestRepresentable?,
                                    decodingProcessor: ModelDecodingProcessor<T>?,
                                    nestedModelGetter: NestedModelGetter? ) -> BaseService<T, ErrorType>
    }
    

    class VKAPIBuilder: APIBuilder {
        typealias ErrorType = VKAPIError
        
        func buildAPI<T: Decodable>(_ responseType: T.Type, request: HTTPRequestRepresentable? = nil,
                 decodingProcessor: ModelDecodingProcessor<T>? = nil,
                 nestedModelGetter: NestedModelGetter? = nil) -> BaseService<T, VKAPIError> {
            let service = BaseService<T, VKAPIError>()
            service.request = request
            
            let responseHandler = HTTPResponseHandler<T, VKAPIError>()
            responseHandler.nestedModelGetter = nestedModelGetter
            responseHandler.successResponseChecker = VKAPISuccessChecker()
            
            if let decodingProcessor = decodingProcessor {
                responseHandler.decodingProcessor = decodingProcessor
            }
            
            service.responseHandler = responseHandler
            
            return service
        }
    }
    


    Теперь завернем все это в отдельный объект, который будет реализовывать следующие протоколы:

    protocol WallGettable {
        func getWall(completion: @escaping (Wall) -> ())
    }
    
    protocol WallPostable {
        func postWall(with message: String)
    }
    
    typealias WallAPI = WallGettable & WallPostable
    

    А вот его реализация:

    class BasicWallAPI: WallAPI {
        private lazy var getWallService: BaseService<Wall, VKAPIError> = {
            return VKAPIBuilder().buildAPI(Wall.self, nestedModelGetter: ResponseModelGetter.wallResponse)
        }()
        
        private lazy var postService: BaseService<[String: [String: Int]], VKAPIError> = {
            return VKAPIBuilder().buildAPI([String: [String: Int]].self)
        }()
    
        func getWall(fromOwnerWith id: String, completion: @escaping (Wall) -> ()) {
            getWallService.request = WallRouter.GET(id, count: 20)
    
            getWallService.sendRequest()?.onSucces({ (wall) in
                completion(wall)
            })
        }
    
        func postWall(with message: String) {
            postService.request = WallRouter.POST(message: message)
            postService.sendRequest()
        }
    }
    


    В моем случае запросы представляют собой структуры типа:

    struct WallRouter {
        struct GET: HTTPGETRequest {
            var path: String = "https://api.vk.com/method/wall.get"
            var parameters: JSON? = [:]
            var headerFields: [String: String]?
            
            init(_ ownerID: String, count: Int) {
                parameters?["owner_id"] = ownerID
                parameters?["count"] = count
            }
        }
        
        struct POST: HTTPPOSTRequest {
            var path: String = "https://api.vk.com/method/wall.post"
            var parameters: JSON? = [:]
            var headerFields: [String: String]?
            
            var bodyString: String? = nil
            
            init(message: String) {
                parameters?["message"] = message
            }
        }
    }
    


    Вот мы и пришли к результату, который был показан в начале статьи. Приятным плюсом является реализация принципа Separation of Concerns, то есть контроллер будет в состоянии вызвать только те методы, которые ему необходимы и не будет знать ни о чем другом.

    Пример использования


    Давайте реализуем также метод для получения количества записей на стене, то есть значения поля count.

    Расширим протокол WallGettable:

    protocol WallGettable {
        func getWall(completion: @escaping (Wall) -> ())
        func getWallItemsCount(completion: @escaping (Int) -> ())
    }
    


    Реализуем новый метод в нашей структуре:
        private lazy var getWallItemsCountService: BaseService<Int, VKAPIError> = {
            return VKAPIBuilder().buildAPI(Int.self, decodingProcessor: IntDecodingProcessor(),
                                       nestedModelGetter: ResponseModelGetter.wallResponseCount)
        }()
        
        func getWallItemsCount(fromOwnerWith id: String, completion: @escaping (Int) -> ()) {
            getWallItemsCountService.request = WallRouter.GET(id, count: 1)
            getWallItemsCountService.sendRequest()?.onSucces({ (count) in
                completion(count)
            })
        }
    

    Что из себя представляет ResponseModelGetter?

    enum ResponseModelGetter: String, NestedModelGetter {
        case wallResponse = "response"
        case wallResponseItems = "response.items"
        case wallResponseCount = "response.count"
        case wallResponseFirstText = "response.items.text"
        
        var keyPath: String {
            return self.rawValue
        }
    }
    

    Вызовем новый метод:

        override func viewDidLoad() {
            super.viewDidLoad()
            
            getWallAPI.getWallItemsCount(fromOwnerWith: "<some id>") { (count) in
                print("Wall items count is \(count)")
            }
        }
    

    Получим вывод в консоль:

    Wall items count is 1650

    Конечно, необязательно оборачивать API всю эту логику, но это удобно, красиво и правильно (главное не свалить все методы API в один класс). При тестировании слоя на API ВК не забудьте вставить свой access_token (куда его прописывать найдете в ReqestPreparator'ах).

    Детали реализации вы можете посмотреть в проекте, доступном по ссылке.

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

    Ну. И что?
    Реклама
    Комментарии 6
    • +1

      Хосспаде, будни быдлокодера велосипедостроителя.

      • +2
        Подскажите, а в чём конкретно заключается гибкость данного примера?
        • –4
          В том, что мы можем легко изменять реализацию того или иного этапа, реализовав нужный протокол и инжектировав реализацию в сетевой модуль. Обычно реализация взаимодействия с серверным API ложится на один большой класс, который делает все, тут за каждый этап отвечает отдельный объект.
          • +2
            Ну во перрвых почему вы решили что «Обычно реализация взаимодействия с серверным API ложится на один большой класс»? Это далеко не так. Если это единственное что подразумевает гибкость, то я разочарован.
        • +1
          А чем ваша реализация лучше Alamofire?
          • –2

            Alamofire это фреймворк для работы с сетью, а не архитектурный слой. С сетью мы работаем в рамках слоя, ваш вопрос не корректный, в начале статьи обращено на это внимание.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое