it-swarm.com.ru

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

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

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

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

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

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

306
MainstreamDeveloper00

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

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

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

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

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

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

АРХИТЕКТУРА

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

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

После всех этих действий на уровне Service вызывающая сторона (контроллер представления) может выполнить некоторые сложные асинхронные действия с ответом: манипулирование сигналом, сцепление, отображение и т.д. С помощью примитивов ReactiveCocoa, или просто подписаться на него и показать результаты в Посмотреть. Я вставляю с помощью Dependency Injection во все эти классы обслуживания мой APIClient, который преобразует конкретный вызов службы в соответствующий запрос GET, POST, PUT, DELETE и т.д. К REST конечная точка. В этом случае APIClient неявно передается всем контроллерам, вы можете сделать это явным образом с помощью параметризованных классов обслуживания APIClient. Это может иметь смысл, если вы хотите использовать различные настройки APIClient для определенных классов обслуживания, но если по каким-то причинам вам не нужны дополнительные копии или вы уверены, что всегда будете использовать один конкретный экземпляр (без настроек) APIClient - сделать его одиночным, но НЕ делайте, пожалуйста, НЕ делайте классы обслуживания как одиночные.

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

  1. Право собственности на один экземпляр не может быть разумно присвоено;
  2. Ленивая инициализация желательна;
  3. Глобальный доступ не предусмотрен иным образом.

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

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

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

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

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

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

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

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

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

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

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

Я описываю вам "старый" пример Objective-C, но этот подход может быть очень легко адаптирован для языка Swift с гораздо большим количеством улучшений, потому что он имеет более полезные функции и функциональный сахар. Я настоятельно рекомендую использовать эту библиотеку: Моя . Это позволяет вам создать более элегантный слой APIClient (наша рабочая лошадка, как вы помните). Теперь наш провайдер APIClient будет типом значения (enum) с расширениями, соответствующими протоколам и использующими сопоставление с образцом деструктуризации. Swift enums + сопоставление с образцом позволяет нам создавать алгебраические типы данных как в классическом функциональном программировании. Наши микросервисы будут использовать этого улучшенного поставщика 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. Это лучший совет, который я могу вам дать.

315
Oleksandr Karaberov

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

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

Архитектура нашего общего приложения для iOS основана на следующих шаблонах: Сервисные уровни , MVVM , Привязка данных пользовательского интерфейса , Внедрение зависимостей знак равно и функционально-реактивное программирование парадигма.

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

  • Сборочный
  • Модель
  • Сервисы
  • Место хранения
  • Менеджеры
  • Координаторы
  • UI
  • Инфраструктура

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

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

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

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

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

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

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

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

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

  1. Проверить сообщение (слой модели)
  2. Сохранить сообщение локально (хранилище сообщений)
  3. Загрузить вложение сообщения (сервис Amazon s3)
  4. Обновите статус сообщения и URL-адреса вложений и сохраните сообщение локально (хранилище сообщений)
  5. Сериализация сообщения в формате JSON (слой модели)
  6. Опубликовать сообщение в PubNub (сервис PubNub)
  7. Обновите статус и атрибуты сообщений и сохраните их локально (хранилище сообщений)

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

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

  1. ViewModels
  2. ViewControllers
  3. Просмотры

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

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

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

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


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

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

Также вы можете найти больше информации о процессе разработки iOS в этом посте iOS Development as a Service

30
Alex Petropavlovsky

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

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

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

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

18
Rickye

В моей ситуации я обычно использую библиотеку 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.

8
Andrew Cherkashyn

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

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

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

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

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

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

7
Fran K.

Я использую подход, который я получил здесь: 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 и добавляет его в очередь.

4
bzz

Мы используем несколько подходов в зависимости от ситуации. Для большинства вещей 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 либо встроенным, если вы делаете один вызов, либо абстрагируете в класс запроса/ответа.

3
Martin

Я избегаю одиночек при разработке своих приложений. Они типичны для многих людей, но я думаю, что вы можете найти более элегантные решения в других местах. Обычно я создаю свои сущности в 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];
}

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

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

1
Sandy Chapman

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

Alamofire для Swift. https://github.com/Alamofire/Alamofire

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

0
matt.writes.code

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

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

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

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

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

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

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

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

0
Nirav Bhatt

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

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

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

Выдвижной для разных протоколов.

0
Kevin