➡️🍏 Сетевые запросы и REST API в iOS и Swift: протокольно-ориентированное программирование. Часть 2
В заключительной части обсудим, как избежать повторения кода, решить проблему загрязненного интерфейса и как абстрагировать ресурсы API с помощью протоколов, дженериков и расширений.
Глава 4: Протокольно-ориентированная архитектура сетевого слоя
Стандартные архитектурные подходы к сетевому уровню приложения для iOS нарушают общие принципы проектирования и создают код, полный повторений, который необходимо постоянно менять, чтобы освободить место для новых сетевых запросов и типов данных. Следуя протокольно-ориентированному подходу, мы можем избежать всех этих проблем.
Ресурсы API должны быть типами моделей
Показанный выше подход сетевого менеджера — не единственный, который можно найти в интернете, хотя он довольно стандартный.
Для решения проблем, показанных выше, вы можете вынести в отдельную структуру ресурсов все параметры, которые вызывают длинные условия или повторение кода.
Затем эту структуру ресурсов можно передать универсальному методу, который выполняет сетевой запрос.
Код обычно выглядит следующим образом:
Это, безусловно, шаг в правильном направлении, но полностью проблему не решает. Несмотря на то что произошло явное улучшение, остается еще одна проблема: структура ресурса перегружена кучей свойств и методов для представления всех возможных параметров и декодирования данных различными способами.
Имейте в виду: проблема не в количестве свойств или методов. Проблема во взаимоисключении.
Например, метод, который декодирует двоичные данные в изображения, нельзя использовать, если представленный ресурс возвращает данные JSON, и наоборот. Вызывающая сторона должна знать это, потому что тип раскрывает интерфейс, который должен быть использован конкретным, но еще не определенным способом.
Эта проблема называется загрязнением интерфейса (interface pollution), которая возникает, когда тип использует методы, которые ему не нужны. Загрязнение интерфейса — это следствие нарушения другого принципа SOLID, принципа разделения интерфейса .
Решение здесь состоит в том, чтобы разделить ресурсы на несколько типов, которые затем могут совместно использовать стандартный интерфейс и функциональные возможности посредством программно-ориентированного программирования.
Абстрагирование ресурсов API с помощью протоколов, дженериков и расширений
Начнем с ресурсов, предоставляемых REST API. Все удаленные ресурсы, независимо от их типа, имеют общий стандартный интерфейс. Ресурс имеет:
- URL-адрес, заканчивающийся путем, указывающим данные, которые мы извлекаем (например, запрос)
- опциональные параметры для фильтрации или сортировки данных в ответе;
- связанный тип модели, в который необходимо преобразовать данные.
Мы можем указать все эти требования, используя протокол. Затем, с расширением протокола, мы можем предоставить общую реализацию.
Вычисляемое url
-свойство собирает полный URL-адрес ресурса, используя базовый URL-адрес API, параметр methodPath
и различные параметры запроса.
Обратите внимание, что в приведенном выше коде большинство параметров жестко запрограммированы, поскольку они всегда одинаковы. Единственным исключением является необязательное свойство filter
, которое нам понадобится в одних запросах, но не понадобится в других. Вы можете быстро преобразовать любой другой параметр в требование для протокола ApiResource
, если вам нужен более детальный контроль над ним.
Благодаря этому протоколу, теперь легко создавать конкретные структуры для вопросов, ответов, пользователей или любого другого типа, предлагаемого API Stack Overflow.
В нашем небольшом примере приложения нам нужен только ресурс для вопросов.
Эта структура содержит всю логику, связанную с удаленным ресурсом:
- Если указан идентификатор, мы запрашиваем данные одного запроса. В противном случае нам нужен список.
- Когда мы запрашиваем данные для одного запроса, мы включаем фильтр, который заставляет удаленный API возвращать дополнительные данные. В нашем случае это тело запроса.
Обратите также внимание на то, что и расширение протокола APIResource
, и расширение QuestionsResource
не содержат асинхронного кода, что значительно упрощает модульное тестирование.
Создание универсальных классов для выполнения вызовов API и других сетевых запросов.
Теперь, когда у нас есть представление ресурсов, предлагаемых API, нам действительно нужно создать несколько сетевых запросов.
Как мы видели, не все наши сетевые запросы отправляются в REST API. Медиафайлы обычно хранятся на CDN. Это означает, что нам нужно, чтобы наш сетевой код был универсальным и не был привязан к протоколу APIResource
, который мы создали выше.
Итак, мы начинаем с анализа требований с точки зрения вызывающей стороны. Общий сетевой запрос требует:
- метод преобразования полученных данных в тип модели;
- способ запуска асинхронной передачи данных;
- обратный вызов для передачи обработанных данных обратно вызывающей стороне.
Мы снова объявляем эти требования, используя протокол:
Благодаря этим требованиям, мы можем абстрагировать код, который использует URLSession
для выполнения сетевой передачи. Мы снова помещаем этот код в расширение протокола:
Не забудьте добавить слабую ссылку на себя в список захвата обработчика завершения любого асинхронного метода, например, dataTask(with:completionHandler)
или вы можете вызвать утечку памяти или неожиданные ошибки.
Как и ресурсы API, наши конкретные классы сетевых запросов будут основаны на протоколе NetworkRequest
, предоставляя недостающие части, определенные требованиями протокола.
Самый простой тип сетевого запроса — это запрос для изображений, для которого нам нужен только URL:
Создать значение UIImage
из полученного Data
несложно. И поскольку нам не нужна какая-то конкретная конфигурация, метод execute(withCompletion:)
из ImageRequest
может просто вызвать метод load(_:withCompletion:)
из NetworkRequest
.
Вы снова видите, что этот подход позволяет нам создавать столько типов запросов, сколько нам нужно. Все, что нам нужно сделать, это добавить новые классы. Нет необходимости изменять существующий код, соблюдая принцип открытости-закрытости.
Теперь мы можем следовать тому же процессу и создать класс для запросов API.
Класс APIRequest
использует универсальный тип Resource
. Единственное требование состоит в том, чтобы ресурсы соответствовали APIResource
. Соответствие протоколу NetworkRequest
также не было таким сложным. Поскольку API возвращает данные JSON, все, что нам нужно сделать, это расшифровать полученное Data
с помощью класса JSONDecoder
.
Теперь у нас есть расширяемая протокольно-ориентированная архитектура, которую мы можем расширять по своему усмотрению. Мы можем добавлять новые ресурсы API по мере необходимости или новые типы сетевых запросов для отправки данных или загрузки других типов медиафайлов.
Глава 5: Выполнение сетевых запросов в реальном приложении SwiftUI
После создания полностью рабочего сетевого уровня получение данных в приложении SwiftUI становится простым. Все, что вам нужно сделать, это собрать вместе различные части, которые скроют все детали реализации от ваших представлений и моделей данных SwiftUI.
Выполнение сетевых запросов внутри моделей представления.
Наконец-то мы подошли к последней фазе этой длинной статьи. Здесь мы будем получать данные из API Stack Exchange и отображать их на экране.
Мы собираемся выполнять сетевые запросы в моделях представления/данных, следуя паттерну MVVM. Хотя это также требует подробного объяснения, этот вопрос выходит за рамки данной статьи. Вы можете обратиться к моей другой статье, чтобы углубиться в паттерн.
Начнем с первого экрана нашего приложения.
Прежде всего, нам нужен объект модели данных, который извлекает самые популярные вопросы из Stack Overflow.
Обратите внимание, что на этом уровне все, что нам нужно сделать, это:
• создать переменную QuestionsResource
;
• передать ее новому экземпляру APIRequest
;
• выполнить сетевой запрос;
• и сохранять возвращенные вопросы в свойстве @Published
для обновления пользовательского интерфейса.
То, как работает наша сетевая инфраструктура, полностью скрыто от нашей модели представления. Нет нужды беспокоиться о сеансах, URL-адресах или декодировании JSON.
Запуск сетевых запросов в представлениях SwiftUI и заполнение пользовательского интерфейса
Теперь мы можем позаботиться о пользовательском интерфейсе. Давайте начнем с некоторых расширений для форматирования данных в наших представлениях.
Нам также нужны некоторые данные для заполнения наших предварительных просмотров Xcode. Мы можем получить некоторые данные JSON непосредственно из API Stack Exchange и сохранить их в файле с именем Questions.json. Затем мы загружаем его в специальную структуру с помощью файла JSONdecoder
.
Поскольку оба экрана в нашем приложении имеют общие элементы дизайна, мы можем создать повторно используемое представление.
И, наконец, мы можем собрать представление на весь экран.
QuestionView
в приведенном выше коде еще не существует, поэтому ваш код не будет скомпилирован. Если вы хотите запустить приложение на симуляторе и увидеть, как экран заполняется вопросами, замените QuestionView
на EmptyView
или TextView
.
Последовательность асинхронных сетевых запросов
Представление, показывающее подробности вопроса, работает в основном таким же образом, но с существенным отличием. Ему необходимо последовательно выполнить два сетевых запроса: один для тела вопроса и один для изображения профиля владельца вопроса.
Это одна из причин, по которой некоторым разработчикам нравится использовать внешние библиотеки или фреймворк FRP, например, Combine. Чтобы упорядочить два сетевых запроса, вы должны вложить второй в замыкание завершения первого.
Это может легко привести к проблемам с обратным вызовом.
Но вам не нужна какая-то сложная структура, чтобы решить эту проблему. Вместо этого поместите каждый сетевой запрос в отдельный метод. Это также поможет вам сохранить хорошую организацию кода.
Опять же, оба метода только создают запрос и выполняют его. Детали реализации не просачиваются в наши модели представления.
Мы почти закончили. Все, что нам осталось, это построить экран для одного запроса.
Прежде всего, давайте создадим представление для отображения данных владельца, включая его изображение профиля.
Затем мы собираем полное представление, используя представления Owner и Details (создавали в предыдущем разделе).
Пока данные загружаются, мы показываем ProgressView
и скрываем остальную часть пользовательского интерфейса. Это простое решение, но вы можете усложнить его настолько, насколько захотите.
Важно то, что мы берем информацию о прогрессе из модели данных. Представлению не нужно ничего знать о выполняемых сетевых запросах или асинхронном коде.
Если у вас быстрое подключение к Интернету, пользовательский интерфейс может появиться сразу. Чтобы увидеть, как сетевое приложение работает с медленными соединениями, вы можете использовать network link conditioner, чтобы замедлить ваши сетевые запросы.
Вывод
В этой статье я показал вам не только как отправлять сетевые запросы к удаленному REST API, но и как структурировать сетевой уровень в ваших приложениях. Хотя пример приложения, которое мы создали — простой, вы могли заметить, что оно уже сопряжено с большой сложностью. Правильное проектирование сетевого кода — это инвестиция, которая принесет хорошие дивиденды в будущем, поскольку добавление новых вызовов API в приложение становится простым, с более высокой долей повторного использования кода.
Важные концепции, которые следует помнить:
- REST API основан на URL-адресах и протоколе HTTP.
Архитектура REST для веб-служб использует URL-адреса для указания ресурсов и параметров и методы HTTP для идентификации действий. В ответах используются коды состояния HTTP для выражения результатов и тело ответа для возврата запрошенных данных, часто в формате JSON.
- Вам не нужна сетевая библиотека, такая как AlamoFire или AFNetworking.
Внешние библиотеки добавляют в ваше приложение зависимости и ограничения. Сторонние библиотеки могут изменяться или ломаться без предварительного уведомления, и вам придется адаптировать свою архитектуру к выбору решения, сделанного кем-то другим.
- Вы выполняете сетевые запросы в iOS, используя систему загрузки URL.
Есть три типа, которые вам нужны для выполнения сетевых запросов. Класс URLSession обрабатывает сеансы HTTP, структура URLRequest представляет один запрос в сеансе, а класс URLSessionTask— это тип, который выполняет асинхронную передачу данных.
- Монолитные сетевые менеджеры создают проблемы в вашем коде.
Многие разработчики помещают весь сетевой код в один класс менеджера. Это нарушает здравые принципы разработки программного обеспечения и создает код, который трудно изменить, легко сломать и трудно протестировать.
- Протокольно-ориентированное программирование — лучший инструмент для построения сетевого уровня ваших приложений.
Используя комбинацию протоколов, расширений и конкретных типов, вы можете создать гибкую иерархию, которую легко расширить с помощью новых ресурсов и сетевых запросов.