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

Я разработчик iOS с некоторым опытом, и этот вопрос мне очень интересен. Я видел много разных ресурсов и материалов по этой теме, но тем не менее я все еще смущен. Какая лучшая архитектура для сетевого приложения iOS? Я имею в виду базовую абстрактную структуру, шаблоны, которые будут соответствовать каждому сетевому приложению, будь то небольшое приложение, в котором есть только несколько запросов сервера или сложный клиент REST. Apple рекомендует использовать MVC как базовый архитектурный подход для всех приложений iOS, но ни MVC, ни более современные шаблоны MVVM не объясняют, куда поместить сетевой логический код и как его организовать в целом.

Мне нужно разработать что-то вроде MVCS (S для Service), а на этом уровне Service положить все запросы API и другую сетевую логику, которая в перспективе может быть действительно сложной? Проведя некоторые исследования, я нашел для этого два основных подхода. Здесь было рекомендовано создать отдельный класс для каждого сетевого запроса к веб-сервису API (например, класс LoginRequest class или PostCommentRequest и т.д.), который все наследует от абстрактного класса базового запроса AbstractBaseRequest и в дополнение к созданию некоторого глобального сетевого менеджера, который инкапсулирует общий сетевой код и другие настройки (может быть настройка AFNetworking или RestKit, если у нас есть сложные отображения объектов и постоянство или даже собственную реализацию сетевой коммуникации со стандартным API). Но этот подход кажется для меня накладными. Другим подходом является наличие диспетчера или диспетчера oneton API, как в первом подходе, , но не для создания классов для каждого запроса и вместо этого для инкапсуляции каждого запроса в качестве общедоступного метода экземпляра этого класса менеджера например: fetchContacts, loginUser методы и т.д. Итак, что является лучшим и правильным способом? Есть еще другие интересные подходы, которые я еще не знаю?

И должен ли я создать еще один слой для всего этого сетевого материала, такого как Service или NetworkProvider или что-то еще поверх моей архитектуры MVC, или этот слой должен быть интегрирован (добавлен) в существующие слои MVC например Model?

Я знаю, что существуют красивые подходы, или как тогда такие мобильные монстры, как клиент Facebook или клиент LinkedIn, сталкиваются с экспоненциально растущей сложностью сетевой логики?

Я знаю, что нет точного и формального ответа на проблему. Цель этого вопроса - собрать наиболее интересные подходы от опытных разработчиков iOS. Наилучший предлагаемый подход будет отмечен как принятый и отмеченный наградой за репутацию, другие будут поддержаны. Это в основном теоретический и исследовательский вопрос. Я хочу понять базовый, абстрактный и правильный архитектурный подход для сетевых приложений в iOS. Я надеюсь получить подробное объяснение от опытных разработчиков.

Ответ 1

I want to understand basic, abstract and correct architectural approach for networking applications in iOS: для построения архитектуры приложения нет "лучшего" или "наиболее правильного" подхода. Это очень творческая работа. Вы всегда должны выбирать наиболее простую и расширяемую архитектуру, которая будет понятна любому разработчику, который начнет работать над вашим проектом или другими разработчиками в вашей команде, но я согласен, что может быть "хороший" и "плохой" "архитектура.

Вы сказали: collect the most interesting approaches from experienced iOS developers, я не думаю, что мой подход является самым интересным или правильным, но я использовал его в нескольких проектах и ​​доволен этим. Это гибридный подход тех, которые вы упомянули выше, а также улучшения моих собственных исследований. Мне интересны проблемы построения подходов, которые объединяют несколько известных паттернов и идиом. Я думаю, что многие из корпоративных шаблонов Fowler могут быть успешно применены к мобильным приложениям. Вот список наиболее интересных, которые мы можем применить для создания архитектуры приложений iOS (на мой взгляд): Уровень обслуживания, Unit of Work, Удаленный фасад, Объект передачи данных, Gateway, Supertype слоя, Специальный случай, Модель домена. Вы всегда должны правильно проектировать слой модели и всегда не забывать о сохранении (это может значительно увеличить производительность вашего приложения). Вы можете использовать Core Data для этого. Но вы не должны забывать, что Core Data не ORM или база данных, а менеджер графа объектов с сохранением как хороший вариант. Поэтому очень часто Core Data может быть слишком тяжелым для ваших нужд, и вы можете посмотреть на новые решения, такие как Realm и Couchbase Lite или создайте свой собственный слой для сопоставления/сохранения переносимых объектов на основе raw SQLite или LevelDB. Также я советую вам ознакомиться с Domain Driven Design и CQRS.

Сначала, я думаю, мы должны создать еще один слой для сетей, потому что нам не нужны контроллеры жира или тяжелые, перегруженные модели. Я не верю в эти вещи. Но я верю в подход skinny everything, потому что ни один класс не должен быть толстым, никогда. Вся сеть может быть в целом абстрагирована как бизнес-логика, поэтому у нас должен быть еще один слой, где мы можем это выразить. Уровень обслуживания - это то, что нам нужно:

It encapsulates the application business logic,  controlling transactions 
and coordinating responses in the implementation of its operations.

В нашей области MVC Service Layer есть нечто вроде посредника между моделью домена и контроллерами. Существует довольно схожая вариация этого подхода под названием MVCS, где Store на самом деле является нашим слоем Service. Store отображает экземпляры модели и обрабатывает сети, кэширование и т.д. Я хочу упомянуть, что вы не должны писать всю свою сетевую и бизнес-логику на уровне обслуживания. Это также можно считать плохим дизайном. Для получения дополнительной информации смотрите Anemic и Rich модели домена. Некоторые методы обслуживания и бизнес-логику можно обрабатывать в модели, поэтому она будет "богатой" (с поведением) моделью.

Я всегда широко использую две библиотеки: AFNetworking 2.0 и ReactiveCocoa. Я думаю, что это необходимо для любого современного приложения, которое взаимодействует с сетью и веб-службами или содержит сложную логику пользовательского интерфейса.

АРХИТЕКТУРА

Сначала я создаю общий класс APIClient, который является подклассом AFHTTPSessionManager. Это рабочая лошадка всей сети в приложении: все классы обслуживания делегируют на нее действительные запросы REST. Он содержит все настройки HTTP-клиента, которые мне нужны в конкретном приложении: привязка SSL, обработка ошибок и создание простых объектов NSError с подробными причинами сбоев и описаниями всех API и ошибок подключения (в этом случае контроллер будет способный отображать правильные сообщения для пользователя), установка сериализаторов запросов и ответов, заголовков HTTP и других связанных с сетью материалов. Затем я логически разделяю все запросы API на подсервисы или, вернее, microservices: UserSerivces, CommonServices, SecurityServices, FriendsServices и т.д., соответственно, реализуемой бизнес-логикой. Каждый из этих микросервисов является отдельным классом. Они вместе образуют a Service Layer. Эти классы содержат методы для каждого запроса API, модели домена процесса и всегда возвращают вызывающему абоненту RACSignal с моделью синтаксического анализа или NSError.

Я хочу упомянуть, что если у вас сложная логика сериализации модели, то создайте для нее еще один слой: нечто вроде Data Mapper, но более общее, например. JSON/XML → Модельный преобразователь. Если у вас есть кеш: тогда создайте его как отдельный слой/службу тоже (вы не должны смешивать бизнес-логику с кешированием). Зачем? Потому что правильный слой кеширования может быть довольно сложным с его собственными ошибками. Люди реализуют сложную логику, чтобы получить достоверное, предсказуемое кэширование, например, моноидальное кэширование с проекциями на основе профинансов. Вы можете прочитать эту красивую библиотеку под названием Carlos, чтобы понять больше. И не забывайте, что Core Data может действительно помочь вам со всеми проблемами кеширования и позволит вам писать меньше логики. Кроме того, если у вас есть логика между моделями запросов NSManagedObjectContext и сервера, вы можете использовать шаблон Repository, который отделяет логику, которая извлекает данных и сопоставляет их с моделью сущности из бизнес-логики, которая действует на модель. Поэтому я советую использовать шаблон репозитория, даже если у вас есть базовая архитектура на основе данных. Репозиторий может абстрагировать такие вещи, как NSFetchRequest, NSEntityDescription, NSPredicate и т.д. До простых методов, таких как get или put.

После всех этих действий на уровне службы вызывающий (контроллер представления) может выполнять сложный асинхронный материал с ответом: манипуляции с сигналами, привязку, сопоставление и т.д. с помощью примитивов ReactiveCocoa или просто подписаться на него и показать результаты в представлении. Я ввожу Dependency Injection во всех этих классах сервиса my APIClient, который переводит конкретный вызов службы в соответствующие get, POST, put, DELETE и т.д. запрос конечной точке REST. В этом случае APIClient передается неявно всем контроллерам, вы можете сделать это явным с параметризованным над APIClient служебными классами. Это может иметь смысл, если вы хотите использовать различные настройки APIClient для определенных классов услуг, но если по каким-либо причинам вы не хотите дополнительных копий или вы уверены, что всегда будете использовать один конкретный экземпляр (без настроек ) APIClient - сделать его одноэлементным, но НЕ НУЖНО, НЕ ОБРАЩАЙТЕСЬ к классам обслуживания, как к синглонам.

Затем каждый контроллер представления снова с DI вводит требуемый класс обслуживания, вызывает соответствующие методы обслуживания и составляет свои результаты с логикой пользовательского интерфейса. Для инъекций зависимостей мне нравится использовать BloodMagic или более мощную структуру Typhoon. Я никогда не использую одноточие, класс Бога APIManagerWhatever или другие неправильные вещи. Потому что, если вы вызываете свой класс WhateverManager, это указывает на то, что вы не знаете его цели, и это плохой выбор дизайна. Синглтоны также являются анти-шаблонами, а в наиболее случаях (за исключением редких) является неправильным решением. Синглтон следует рассматривать только в том случае, если удовлетворяются все три из следующих критериев:

  • Собственность одного экземпляра не может быть разумно назначена;
  • Желательна ленивая инициализация;
  • В глобальном доступе не предусмотрено иное.

В нашем случае право собственности на единственный экземпляр не является проблемой, а также нам не нужен глобальный доступ после того, как мы разделили наш менеджер богов на сервисы, потому что теперь только одному или нескольким выделенным контроллерам нужна определенная услуга (например, UserProfile для контроллера требуется UserServices и т.д.).

Мы всегда должны уважать принцип S в SOLID и использовать разделение проблем, поэтому не ставьте все свои методы обслуживания и сети в один класс, потому что это сходит с ума, особенно если вы разрабатываете большое корпоративное приложение. Вот почему мы должны учитывать подход внедрения инъекций и услуг. Я рассматриваю этот подход как современный и post-OO. В этом случае мы разделяем наше приложение на две части: логику управления (контроллеры и события) и параметры.

Одним из параметров являются обычные параметры данных. Это то, что мы передаем в функции, манипулируем, модифицируем, сохраняем и т.д. Это сущности, агрегаты, коллекции, классы case. Другим видом будут "сервисные" параметры. Это классы, которые инкапсулируют бизнес-логику, позволяют общаться с внешними системами, обеспечивают доступ к данным.

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

- (RACSignal *)removeFriend:(Friend * const)friend

где Friend - объект модели/домена (или он может быть просто объектом User, если у них есть аналогичные атрибуты). Underhood этот метод анализирует Friend до NSDictionary параметров JSON friend_id, name, surname, friend_request_id и так далее. Я всегда использую библиотеку Mantle для этого типа шаблона и для моего модельного слоя (синтаксический анализ назад и вперед, управление иерархиями вложенных объектов в JSON и т.д. на). После разбора он вызывает метод APIClient DELETE, чтобы сделать реальный запрос REST, и возвращает Response в RACSignal вызывающему абоненту (FriendsViewController в нашем случае), чтобы отобразить соответствующее сообщение для пользователя или что-то еще.

Если наше приложение является очень большим, нам нужно еще разделить нашу логику. Например. не всегда удобно смешивать Repository или логику модели с Service one. Когда я описал свой подход, я сказал, что метод removeFriend должен быть в слое Service, но если мы будем более педантичными, мы можем заметить, что он лучше относится к Repository. Пусть вспомнит, что такое Репозиторий. Эрик Эванс дал ему точное описание в своей книге [DDD]:

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

Итак, Repository - это, по сути, фасад, который использует семантику стиля коллекции (Add, Update, Remove) для обеспечения доступа к данным/объектам. Поэтому, когда у вас есть что-то вроде: getFriendsList, getUserGroups, removeFriend, вы можете поместить его в Repository, потому что семантика, подобная коллекции, здесь довольно понятна. И код вроде:

- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;

- это, безусловно, бизнес-логика, потому что она находится за базовыми операциями CRUD и соединяет два объекта домена (Friend и Request), поэтому он должен быть помещен в слой Service. Также хочу заметить: не создавать ненужные абстракции. Используйте все эти подходы с умом. Потому что, если вы подавите свое приложение абстракциями, это увеличит его случайную сложность, а сложность вызывает больше проблем в программных системах, чем что-либо еще

Я опишу вам пример "старого" Objective-C, но этот подход может быть очень легко адаптирован для языка Swift с гораздо большим количеством улучшений, поскольку он имеет более полезные функции и функциональный сахар. Я настоятельно рекомендую использовать эту библиотеку: Moya. Это позволяет вам создать более элегантный слой APIClient (наша рабочая лошадка, как вы помните). Теперь наш поставщик APIClient будет типом значения (enum) с расширениями, соответствующими протоколам и использующим сопоставление шаблонов деструкции. Swift enums + pattern matching позволяет нам создавать алгебраические типы данных, как в классическом функциональном программировании. Наши микросервисы будут использовать этот улучшенный поставщик APIClient, как в обычном подходе Objective-C. Для модельного слоя вместо Mantle вы можете использовать библиотеку ObjectMapper или мне нравится использовать более элегантные и функциональные Argo.

Итак, я описал свой общий архитектурный подход, который, я думаю, может быть адаптирован для любого приложения. Конечно, может быть намного больше улучшений. Я советую вам изучить функциональное программирование, потому что вы можете извлечь из этого много пользы, но не заходите слишком далеко. Устранение избыточного, общего глобального изменчивого состояния, создание неизменяемой модели доменаили создание чистых функций без внешних побочных эффектов, как правило, является хорошей практикой, и новый язык Swift поощряет это. Но всегда помните, что перегрузка кода с тяжелыми чистыми функциональными шаблонами, теоретико-теоретические подходы - это плохая идея, потому что другие разработчики будут читать и поддерживать ваш код, и они могут быть расстроены или страшны в отношении prismatic profunctors и таких вещей в вашей неизменной модели. То же самое с ReactiveCocoa: не RACify ваш код слишком много, потому что он может стать нечитаемым очень быстро, особенно для новичков, Используйте его, когда он действительно может упростить ваши цели и логику.

Итак, read a lot, mix, experiment, and try to pick up the best from different architectural approaches. Это лучший совет, который я могу вам дать.

Ответ 2

В соответствии с целью этого вопроса я хотел бы описать наш подход к архитектуре.

Архитектурный подход

Наша общая архитектура приложений iOS основана на следующих шаблонах: Уровни обслуживания, MVVM, привязка данных пользовательского интерфейса, Инъекция зависимостей; и Функциональное реактивное программирование.

Мы можем разрезать типичное приложение, обращенное к потребителю, на следующие логические слои:

  • Сборка
  • Model
  • Услуги
  • Хранение
  • Менеджеры
  • Координаторы
  • интерфейс
  • Инфраструктура

Слой сборки является начальной точкой нашего приложения. Он содержит контейнер для инъекций зависимостей и объявления объектов приложений и их зависимостей. Этот слой также может содержать конфигурацию приложений (URL-адреса, сторонние ключи служб и т.д.). Для этого мы используем библиотеку Typhoon.

Уровень модели содержит классы моделей доменов, проверки, сопоставления. Мы используем библиотеку Mantle для сопоставления наших моделей: она поддерживает сериализацию/десериализацию в формате JSON и NSManagedObject. Для проверки и представления формы наших моделей мы используем библиотеки FXForms и FXModelValidation.

Уровень сервиса объявляет службы, которые мы используем для взаимодействия с внешними системами, чтобы отправлять или получать данные, представленные в нашей модели домена. Обычно у нас есть службы для связи с API-интерфейсами серверов (для каждого объекта), службами обмена сообщениями (например, PubNub), услугами хранения (например, Amazon S3) и т.д. В основном услуги обертывания объектов, предоставляемых SDK (например, PubNub SDK) или реализовать свою собственную логику связи. Для общих сетей мы используем библиотеку AFNetworking.

Уровень хранилища предназначен для организации локального хранения данных на устройстве. Мы используем Core Data или Realm для этого (оба имеют плюсы и минусы, решение о том, что использовать, основано на конкретных спецификациях). Для настройки Core Data мы используем MDMCoreData библиотеку и набор классов - хранилищ - (похожих на службы), которые обеспечивают доступ к локальному хранилищу для каждого объекта. Для Realm мы просто используем аналогичные хранилища для доступа к локальному хранилищу.

Уровень менеджеров - это место, где живут наши абстракции/обертки.

В роли менеджера может быть:

  • Диспетчер учетных данных с его различными реализациями (keychain, NSDefaults,...)
  • Текущий менеджер сеансов, который знает, как сохранить и предоставить текущий сеанс пользователя.
  • Capture Pipeline, обеспечивающий доступ к медиа-устройствам (видеозапись, аудио, съемка)
  • Менеджер BLE, который обеспечивает доступ к службам Bluetooth и периферийным устройствам.
  • Geo Location Manager
  • ...

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

Мы стараемся избегать синглтонов, но этот слой является местом, где они живут, если они необходимы.

Уровень координаторов предоставляет объекты, которые зависят от объектов из других слоев (Service, Storage, Model), чтобы объединить их логику в одну последовательность работы, необходимую для определенного модуля (функция, экран, история пользователя или пользовательский опыт). Он обычно выполняет асинхронные операции и знает, как реагировать на их успешные и неудачные случаи. В качестве примера вы можете представить функцию обмена сообщениями и соответствующий объект MessagingCoordinator. Обработка операции отправки сообщений может выглядеть так:

  • Подтвердить сообщение (слой модели)
  • Сохранить сообщение локально (хранилище сообщений)
  • Загрузка сообщения (услуга amazon s3)
  • Обновление состояния сообщений и вложений URL-адресов и сохранения сообщений локально (хранение сообщений)
  • Сериализовать сообщение в формате JSON (слой модели)
  • Опубликовать сообщение в PubNub (услуга PubNub)
  • Обновить статус и атрибуты сообщений и сохранить их локально (хранилище сообщений)

На каждом из вышеперечисленных шагов обрабатывается ошибка соответственно.

Уровень пользовательского интерфейса состоит из следующих подслоев:

  • ViewModels
  • ViewControllers
  • представления

Во избежание массового просмотра контроллеров мы используем шаблон MVVM и реализуем логику, необходимую для представления пользовательского интерфейса в ViewModels. ViewModel обычно имеет координаторов и менеджеров в качестве зависимостей. ViewModels, используемые ViewControllers и некоторые виды представлений (например, ячейки представления таблицы). Клей между ViewControllers и ViewModels - это привязка данных и шаблон команды. Чтобы сделать этот клей, мы используем библиотеку ReactiveCocoa.

Мы также используем ReactiveCocoa и его концепцию RACSignal в качестве интерфейса и возвращаем тип значения всех методов-координаторов, служб, хранилищ. Это позволяет нам цеплять операции, запускать их параллельно или последовательно, и многие другие полезные вещи, предоставляемые ReactiveCocoa.

Мы пытаемся внедрить наше поведение пользовательского интерфейса декларативным способом. Связывание данных и автоматическая компоновка помогают достичь этой цели.

Уровень инфраструктуры содержит все помощники, расширения, утилиты, необходимые для работы приложения.


Этот подход хорошо работает для нас и тех типов приложений, которые мы обычно создаем. Но вы должны понимать, что это всего лишь субъективный подход, который должен быть адаптирован/изменен для конкретной цели команды.

Надеюсь, это поможет вам!

Также вы можете найти более подробную информацию о процессе разработки iOS в этом сообщении в блоге iOS Development как услуга

Ответ 3

Поскольку все приложения iOS различны, я думаю, что здесь есть разные подходы, но я обычно иду так:
Создайте класс центрального менеджера (singleton) для обработки всех запросов API (обычно называемых APICommunicator), а каждый метод экземпляра - это вызов API. И есть один центральный (непубличный) метод:

- (RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

Для записи я использую две основные библиотеки/рамки, ReactiveCocoa и AFNetworking. ReactiveCocoa отлично обрабатывает ответы сети async, вы можете сделать (sendNext:, sendError:, и т.д.).
Этот метод вызывает API, получает результаты и отправляет их через RAC в формате "raw" (например, NSArray, что возвращает AFNetworking).
Затем метод, подобный getStuffList:, который вызвал вышеупомянутый метод, подписывается на него сигналом, анализирует необработанные данные на объекты (с чем-то вроде Motis) и отправляет объекты по одному вызывающему (getStuffList: и аналогичные методы также возвращают сигнал что контроллер может подписаться на).
Подписанный контроллер получает объекты блоком subscribeNext: и обрабатывает их.

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

Ответ 4

В моей ситуации я обычно использую библиотеку ResKit для настройки сетевого уровня. Он обеспечивает простой в использовании синтаксический анализ. Это уменьшает мои усилия по настройке отображения для разных ответов и т.д.

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

MappableEntry.h

@interface MappableEntity : NSObject

+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;

@end

MappableEntry.m

@implementation MappableEntity

+(NSArray*)pathPatterns {
    return @[];
}

+(NSArray*)keyPathes {
    return nil;
}

+(NSArray*)fieldsArrayForMapping {
    return @[];
}

+(NSDictionary*)fieldsDictionaryForMapping {
    return @{};
}

+(NSArray*)relationships {
    return @[];
}

@end

Отношения - это объекты, которые в ответ представляют вложенные объекты:

RelationshipObject.h

@interface RelationshipObject : NSObject

@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;

@end

RelationshipObject.m

@implementation RelationshipObject

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = key;
    object.destination = key;
    object.mappingClass = mappingClass;
    return object;
}

+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = source;
    object.destination = destination;
    object.mappingClass = mappingClass;
    return object;
}

@end

Затем я настраиваю отображение для RestKit следующим образом:

ObjectMappingInitializer.h

@interface ObjectMappingInitializer : NSObject

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;

@end

ObjectMappingInitializer.m

@interface ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses;

@end

@implementation ObjectMappingInitializer

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {

    NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];

    // Creating mappings for classes
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
        [newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
        [newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
        [mappingObjects setObject:newMapping forKey:[mappableClass description]];
    }

    // Creating relations for mappings
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
        for (RelationshipObject *relation in [mappableClass relationships]) {
            [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
        }
    }

    // Creating response descriptors with mappings
    for (Class mappableClass in [self mappableClasses]) {
        for (NSString* pathPattern in [mappableClass pathPatterns]) {
            if ([mappableClass keyPathes]) {
                for (NSString* keyPath in [mappableClass keyPathes]) {
                    [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                }
            } else {
                [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
            }
        }
    }

    // Error Mapping
    RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
    [errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
    for (NSString *pathPattern in Error.pathPatterns) {
        [[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
    }
}

@end

@implementation ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses {
    return @[
        [FruiosPaginationResults class],
        [FruioItem class],
        [Pagination class],
        [ContactInfo class],
        [Credentials class],
        [User class]
    ];
}

@end

Пример примера реализации MappableEntry:

user.h

@interface User : MappableEntity

@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;

- (NSDictionary*)registrationData;

@end

User.m

@implementation User

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
    if (self = [super init]) {
        self.username = username;
        self.email = email;
        self.password = password;
    }
    return self;
}

- (NSDictionary*)registrationData {
    return @{
        @"username": self.username,
        @"email": self.email,
        @"password": self.password
    };
}

+ (NSArray*)pathPatterns {
    return @[
        [NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
        [NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
    ];
}

+ (NSArray*)fieldsArrayForMapping {
    return @[ @"username", @"email", @"password", @"token" ];
}

+ (NSDictionary*)fieldsDictionaryForMapping {
    return @{ @"id": @"userId" };
}

@end

Теперь о завершении запросов:

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

APICallbacks.h

typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);

И пример моего класса APIRequest, который я использую:

LoginAPI.h

@interface LoginAPI : NSObject

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

@end

LoginAPI.m

@implementation LoginAPI

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
    [[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        onSuccess(mappingResult.array);
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        onError(error);
    }];
}

@end

И все, что вам нужно сделать в коде, просто инициализируйте объект API и вызовите его, когда вам это нужно:

SomeViewController.m

@implementation SomeViewController {
    LoginAPI *_loginAPI;
    // ...
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _loginAPI = [[LoginAPI alloc] init];
    // ...
}

// ...

- (IBAction)signIn:(id)sender {
    [_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
        // Success Block
    } onError:^(NSError *error) {
        // Error Block
    }];
}

// ...

@end

Мой код не идеален, но его легко установить и использовать для разных проектов. Если это кому-то интересно, я мог бы потратить некоторое время и сделать универсальное решение для него где-то на GitHub и CocoaPods.

Ответ 5

На мой взгляд, вся архитектура программного обеспечения обусловлена ​​необходимостью. Если это предназначено для обучения или личных целей, тогда решайте главную цель и управляйте архитектурой. Если это работа по найму, то деловая необходимость имеет первостепенное значение. Хитрость заключается в том, чтобы не позволить блестящим вещам отвлекать вас от реальных потребностей. Мне трудно это сделать. В этом бизнесе всегда появляются новые блестящие вещи, и многие из них не полезны, но вы не всегда можете сказать это заранее. Сосредоточьтесь на необходимости и будьте готовы отказаться от плохих выборов, если сможете.

Например, недавно я сделал быстрый прототип приложения для обмена фотографиями для локального бизнеса. Поскольку бизнес-задача заключалась в том, чтобы сделать что-то быстро и грязно, архитектура оказалась в виде кода iOS, чтобы всплывать камера и некоторый сетевой код, прикрепленный к кнопке отправки, которая загружала изображение в хранилище S3 и записывалась в домен SimpleDB. Код был тривиальным, а стоимость минимальна, и клиент имеет масштабируемую коллекцию фотографий, доступную через Интернет с вызовами REST. Дешевое и немое, приложение имело множество недостатков и иногда блокировало пользовательский интерфейс, но было бы отходами сделать больше для прототипа, и он позволяет им развертывать свои сотрудники и генерировать тысячи тестовых изображений без потери производительности и масштабируемости проблемы. Crappy архитектуры, но он идеально подходит и стоит.

Другой проект включал в себя создание локальной защищенной базы данных, которая синхронизируется с корпоративной системой в фоновом режиме, когда сеть доступна. Я создал фоновый синхронизатор, который использовал RestKit, поскольку у него было все, что мне нужно. Но мне пришлось написать так много специального кода для RestKit, чтобы иметь дело с уникальным JSON, что я мог бы сделать все это быстрее, написав собственные преобразования JSON в CoreData. Тем не менее, клиент хотел довести это приложение в доме, и я чувствовал, что RestKit будет похож на рамки, которые они использовали на других платформах. Я ожидаю, чтобы это было хорошим решением.

Опять же, проблема для меня заключается в том, чтобы сосредоточиться на необходимости и позволить определить архитектуру. Я стараюсь избегать использования сторонних пакетов, поскольку они приносят затраты, которые появляются только после того, как приложение некоторое время находилось в поле. Я стараюсь избегать создания иерархии классов, поскольку они редко окупаются. Если я могу написать что-то в разумные сроки, вместо того, чтобы принять пакет, который не подходит идеально, я делаю это. Мой код хорошо структурирован для отладки и надлежащим образом комментируется, но сторонние пакеты редко бывают. С учетом сказанного я считаю, что AF Networking слишком полезна, чтобы игнорировать и хорошо структурировать, хорошо комментировать и поддерживать, и я многому использую! RestKit охватывает множество распространенных случаев, но я чувствую, что был в битве, когда я его использую, и большинство источников данных, с которыми я сталкиваюсь, полны причуд и проблем, которые лучше всего обрабатывать с помощью специального кода. В моих последних приложениях я просто использую встроенные конвертеры JSON и пишу несколько полезных методов.

Один шаблон, который я всегда использую, - это отключение сетевых вызовов от основного потока. Последние 4-5 приложений, которые я выполнил, создали задачу фонового таймера с помощью dispatch_source_create, которая так часто просыпается и выполняет сетевые задачи по мере необходимости. Вам необходимо выполнить некоторую работу по обеспечению безопасности потока и убедиться, что код изменения пользовательского интерфейса отправляется в основной поток. Это также помогает делать ваш вход/инициализацию таким образом, чтобы пользователь не чувствовал себя обремененным или отложенным. Пока это работает довольно хорошо. Я предлагаю изучить эти вещи.

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

Ответ 6

Я использую подход, который я получил отсюда: https://github.com/Constantine-Fry/Foursquare-API-v2. Я переписал эту библиотеку в Swift, и вы можете увидеть архитектурный подход из этих частей кода:

typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

class Foursquare{
    var authorizationCallback: OperationCallback?
    var operationQueue: NSOperationQueue
    var callbackQueue: dispatch_queue_t?

    init(){
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 7;
        callbackQueue = dispatch_get_main_queue();
    }

    func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
        let parameters: Dictionary <String, String> = [
            "venueId":venueID,
            "shout":shout,
            "broadcast":"public"]
        return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
    }

    func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
        let url = self.constructURL(path, parameters: parameters)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = httpMethod
        let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
        self.operationQueue.addOperation(operation)
        return operation
    }

    func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
        var parametersString = kFSBaseURL+path
        var firstItem = true
        for key in parameters.keys {
            let string = parameters[key]
            let mark = (firstItem ? "?" : "&")
            parametersString += "\(mark)\(key)=\(string)"
            firstItem = false
        }
    return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
    }
}

class Operation: NSOperation {
    var callbackBlock: OpertaionCallback
    var request: NSURLRequest
    var callbackQueue: dispatch_queue_t

    init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
        self.request = request
        self.callbackBlock = callbackBlock
        self.callbackQueue = callbackQueue
    }

    override func main() {
        var error: NSError?
        var result: AnyObject?
        var response: NSURLResponse?

        var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

        if self.cancelled {return}

        if recievedData{
            result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
            if result != nil {
                if result!.isKindOfClass(NSClassFromString("NSError")){
                    error = result as? NSError
            }
        }

        if self.cancelled {return}

        dispatch_async(self.callbackQueue, {
            if (error) {
                self.callbackBlock(success: false, result: error!);
            } else {
                self.callbackBlock(success: true, result: result!);
            }
            })
    }

    override var concurrent:Bool {get {return true}}
}

В принципе, существует подкласс NSOperation, который делает NSURLRequest, анализирует ответ JSON и добавляет блок обратного вызова с результатом в очередь. Основной класс API создает NSURLRequest, инициализирует подкласс NSOperation и добавляет его в очередь.

Ответ 7

Мы используем несколько подходов в зависимости от ситуации. Для большинства целей AFNetworking является самым простым и надежным подходом к тому, что вы можете устанавливать заголовки, загружать многостраничные данные, использовать GET, POST, PUT и DELETE, а для UIKit есть куча дополнительных категорий, которые позволяют вам, например, установить изображение из url. В сложном приложении с большим количеством вызовов мы иногда абстрагируем это до нашего удобного метода, который был бы примерно таким:

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

Существует несколько ситуаций, когда AFNetworking не подходит, например, когда вы создаете инфраструктуру или другой компонент библиотеки, поскольку AFNetworking может уже находиться в другой базе кода. В этой ситуации вы должны использовать NSMutableURLRequest либо inline, если вы делаете один вызов или абстрагированы в класс запроса/ответа.

Ответ 8

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

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

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

В NSManagedObject + Extensions.m:

+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
    NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}

В NSManagedObject + Networking.m:

- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
    [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
    [self handleInputBlocking:blockInput];
}

Зачем добавлять дополнительные вспомогательные классы, если вы можете расширить функциональность общего базового класса через категории?

Если вас интересует более подробная информация о моем решении, дайте мне знать. Я рад поделиться.

Ответ 9

Попробуйте https://github.com/kevin0571/STNetTaskQueue

Создавать запросы API в разделенных классах.

STNetTaskQueue будет обрабатывать потоки и делегирование/обратный вызов.

Расширяется для разных протоколов.

Ответ 10

С точки зрения чисто класса, вы обычно будете иметь что-то вроде этого:

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

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

    Классы модели данных будут полезны не только для отображения данных, но и для их сериализации, причем каждый из них может выставлять свой собственный формат сериализации через методы экспорта JSON/XML/CSV (или что-либо еще).

  • Важно понимать, что вам также нужны классы конструкторов API, которые напрямую сопоставляются с конечными точками API REST. Скажем, у вас есть API, который регистрирует пользователя, поэтому ваш класс API-интерфейса Login API создаст полезную нагрузку POST JSON для входа api. В другом примере класс Builder API-запроса для списка API элементов каталога создаст строку запроса GET для соответствующего api и вызовет запрос REST GET.

    Эти классы-конструкторы API-запросов обычно получат данные из контроллеров представлений, а также передают те же данные обратно для просмотра контроллеров для обновления/других операций UI. Затем контроллеры View решат, как обновлять объекты Data Model с этими данными.

  • Наконец, сердце клиента REST - класс fetcher API, который не обращает внимания на всевозможные запросы API, которые делает ваше приложение. Этот класс, скорее всего, будет синглом, но, как указывали другие, он не должен быть синглом.

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

Ответ 11

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

Аламофир для Свифта. https://github.com/Alamofire/Alamofire

Он создается теми же людьми, что и AFNetworking, но более точно разработан с учетом Swift.

Ответ 12

Я думаю, что пока средний проект использует архитектуру MVVM, а Большой проект использует архитектуру VIPER. и попытаться достичь

  • Протоколно-ориентированное программирование
  • Шаблоны разработки программного обеспечения
  • Принцип С.О.Л.Д.
  • Общее программирование
  • Не повторяйся (СУХОЙ)

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

Разделение для чистого и читабельного кода во избежание дублирования:

import Foundation
enum DataResponseError: Error {
    case network
    case decoding

    var reason: String {
        switch self {
        case .network:
            return "An error occurred while fetching data"
        case .decoding:
            return "An error occurred while decoding data"
        }
    }
}

extension HTTPURLResponse {
    var hasSuccessStatusCode: Bool {
        return 200...299 ~= statusCode
    }
}

enum Result<T, U: Error> {
    case success(T)
    case failure(U)
}

инверсия зависимости

 protocol NHDataProvider {
        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL, completion: @escaping (Result<Codable, DataResponseError>) -> Void)
    }

Основная ответственность:

  final class NHClientHTTPNetworking : NHDataProvider {

        let session: URLSession

        init(session: URLSession = URLSession.shared) {
            self.session = session
        }

        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL,
                             completion: @escaping (Result<Codable, DataResponseError>) -> Void) {
            let urlRequest = URLRequest(url: url)
            session.dataTask(with: urlRequest, completionHandler: { data, response, error in
                guard
                    let httpResponse = response as? HTTPURLResponse,
                    httpResponse.hasSuccessStatusCode,
                    let data = data
                    else {
                        completion(Result.failure(DataResponseError.network))
                        return
                }
                guard let decodedResponse = try? JSONDecoder().decode(Model.self, from: data) else {
                    completion(Result.failure(DataResponseError.decoding))
                    return
                }
                completion(Result.success(decodedResponse))
            }).resume()
        }
    }

Здесь вы найдете архитектуру GitHub MVVM с остальным API Swift Project.