Привет, Хабр. Меня зовут Антон Потапов, я iOS разработчик в компании FINCH. Сегодня я хочу подробно рассказать про то как перевести мобильный проект на GraphQL, описать плюсы и минусы этого подхода. Приступим.
«Легаси». Я думаю каждый слышал это страшное слово, а большинство встречались с ним лицом к лицу. Поэтому не нужно рассказывать насколько сложно интегрировать новые и большие функции в легаси проект.

Один из наших проектов — приложение для крупной лотерейной компании (точно название дать не могу так как NDA). Недавно мне нужно было перевести его на GraphQL, не лишившись рассудка.
Проект очень большой — на первый взгляд, нужно было потратить минимум 200-300 часов, но это выходит за все возможные рамки двухнедельных спринтов. Выделять целый спринт только на одну задачу мы тоже не могли, так как были и побочные фичи, не менее важные чем GraphQL.
Долго думали что же делать и решили переводить проект шаг за шагом, модельку за моделькой. При таком подходе переход будет плавным, а 200-300 часов распределятся на несколько спринтов.
Для работы над проектом я использовал библиотеку Apollo. На Хабре уже есть статья, где описываются все тонкости работы с ним, так что я не буду лишний раз повторяться. Заранее предупреждаю — работа с кодогенерированной моделью не очень удобна и лучше держать её в качестве «сетевой». Каждая сущность содержит поле __typename: String, которое, как ни странно, возвращает имя типа.
Первое что нужно сделать — определить какую легаси-модель мы будем переводить на GraphQL. В моём случае логично было перевести одну из массивных моделей GameInfo, и вложенную в неё DrawInfo.
Закончив с выбором, я решил инициализировать старые легаси модели новыми, полученными с GraphQL. При таком подходе не нужно заменять те части приложения, где используется старая модель. Полностью убрать предыдущие модели было рискованно — не факт, что проект работал бы как раньше, да и времени это заняло бы чересчур много.
Всего можно выделить три этапа внедрения GraphQL:
Как и в любом клиенте, в GraphQLClient должен быть метод fetch, после вызова которого бу��ет выполняться загрузка нужных нам данных.
На мой взгляд, метод fetch для GraphQLClient должен принимать enum, на основе которого будет выполняться соответствующий запрос. Такой подход позволит нам с лёгкостью получать данные для нужной сущности или даже экрана. В случае, если запрос принимает параметры, то они будут передаваться в качестве associated values. При таком подходе проще использовать GraphQL и создавать гибкие запросы.
Наш настроенный GraphQL клиент должен содержать настроенный под существующие особенности ApolloClient, а также метод загрузки fetch, о котором говорилось выше.
Что за QLRequestResult? Это обычный
Метод fetch позволяет обращаться к клиенту через протокол и выполнять switch по requestType. В зависимости от requestType можно задействовать соответствующий приватный метод загрузки, где можно будет преобразовать полученную модель к старой. Например:
В итоге мы получаем готовую, старую модель, полученную из GraphQL.
В документации GraphQL скалярный тип описан следующим образом: «Скалярный тип представляет собой уникальный идентификатор, часто используемый для повторной выборки объекта или в качестве ключа для кэша». Для swift скалярный тип можно легко ассоциировать с typealias.
При написании клиента, я столкнулся с проблемой, что в моей схеме присутствовал скалярный тип Long, который по сути был
Всё бы ничего, но Apollo автоматически приводила все пользовательские скаляры к типу String, что вызывало креш. Проблему я решил так:
В дальнейшем для каждого ��калярного типа нужно добавлять новый typealias. С версией Apollo 14.0 добавили «поддержку для Int кастомных скаляров». Если вы хотите избежать использования данного подхода и флага в кодгене, то ознакомьтесь с решением данной проблемы в гите Apollo.
Чем хорош такой подход? Достаточно быстрый переход на использование GraphQL, относительно подхода с убиранием всех старых моделей.
В дальнейшем, когда нам нужно будет получить данные для какого-либо экрана/модели мы можем обратиться к GraphQLClient и получить необходимые данные.
Из минусов:
В целом переход на GraphQL может оказаться быстрым и безболезненным при хорошо написанном клиенте. У меня он занял около трех дней = 24 рабочих часа. За это время я успел создать клиент, перевести модель и воссоздать модель GameInfo. Описанный подход — не панацея, а сугубо мое решение, реализованное в сжатые сроки.
Если среди вас есть гуру GraphQL, предлагаю поделиться опытом и рассказать насколько удобно использовать GraphQL + Apollo в больших проектах. Или овчинка выделки не стоит?
Спасибо за внимание!
Краткое введение
«Легаси». Я думаю каждый слышал это страшное слово, а большинство встречались с ним лицом к лицу. Поэтому не нужно рассказывать насколько сложно интегрировать новые и большие функции в легаси проект.

Один из наших проектов — приложение для крупной лотерейной компании (точно название дать не могу так как NDA). Недавно мне нужно было перевести его на GraphQL, не лишившись рассудка.
Проект очень большой — на первый взгляд, нужно было потратить минимум 200-300 часов, но это выходит за все возможные рамки двухнедельных спринтов. Выделять целый спринт только на одну задачу мы тоже не могли, так как были и побочные фичи, не менее важные чем GraphQL.
Долго думали что же делать и решили переводить проект шаг за шагом, модельку за моделькой. При таком подходе переход будет плавным, а 200-300 часов распределятся на несколько спринтов.
Технические моменты
Для работы над проектом я использовал библиотеку Apollo. На Хабре уже есть статья, где описываются все тонкости работы с ним, так что я не буду лишний раз повторяться. Заранее предупреждаю — работа с кодогенерированной моделью не очень удобна и лучше держать её в качестве «сетевой». Каждая сущность содержит поле __typename: String, которое, как ни странно, возвращает имя типа.
Декомпозиция задачи
Первое что нужно сделать — определить какую легаси-модель мы будем переводить на GraphQL. В моём случае логично было перевести одну из массивных моделей GameInfo, и вложенную в неё DrawInfo.
- GameInfo — содержит информация о лотерейных играх и тиражах.
- DrawInfo — содержит данные о тираже
Закончив с выбором, я решил инициализировать старые легаси модели новыми, полученными с GraphQL. При таком подходе не нужно заменять те части приложения, где используется старая модель. Полностью убрать предыдущие модели было рискованно — не факт, что проект работал бы как раньше, да и времени это заняло бы чересчур много.
Всего можно выделить три этапа внедрения GraphQL:
- Создание клиента;
- Создание инициализатора для легаси модели;
- Замена запросов к API на GraphQL.
GraphQLClient
Как и в любом клиенте, в GraphQLClient должен быть метод fetch, после вызова которого бу��ет выполняться загрузка нужных нам данных.
На мой взгляд, метод fetch для GraphQLClient должен принимать enum, на основе которого будет выполняться соответствующий запрос. Такой подход позволит нам с лёгкостью получать данные для нужной сущности или даже экрана. В случае, если запрос принимает параметры, то они будут передаваться в качестве associated values. При таком подходе проще использовать GraphQL и создавать гибкие запросы.
enum GraphQLRequest {
case image(ImageId?)
}Наш настроенный GraphQL клиент должен содержать настроенный под существующие особенности ApolloClient, а также метод загрузки fetch, о котором говорилось выше.
func fetch<Response>(requestType: GraphQLRequest, completion: @escaping (QLRequestResult<Response>) -> Void)Что за QLRequestResult? Это обычный
typealias QLRequestResult<Response> = Result<Response, APIError>Метод fetch позволяет обращаться к клиенту через протокол и выполнять switch по requestType. В зависимости от requestType можно задействовать соответствующий приватный метод загрузки, где можно будет преобразовать полученную модель к старой. Например:
private func fetchGameInfo<Response>(gameId: String? = "", completion: @escaping (QLRequestResult<Response>) -> Void) {
//Создаём запрос для Apollo клиента
let query = GetLotteriesQuery(input: gameId)
// Выполняем загрузку данных на глобальной очереди
apolloClient.fetch(query: query, cachePolicy: .returnCacheDataAndFetch, queue: .global()) { result in
switch result {
case .success(let response):
guard let gamesQL = response.data?.games,
let info = gamesQL.info else {
completion(.failure(.decodingError))
return
}
// Мапим Аполовские модельки
let infos: [Games.Info] = info.compactMap({ gameInfo -> Games.Info? in
guard let gameIdRaw = gameInfo?.gameId,
let gameId = GameId(rawValue: gameIdRaw),
let bonusMultiplier = gameInfo?.bonusMultiplier,
let maxTicketCost = gameInfo?.maxTicketCost,
let currentDraws = gameInfo?.currentDraws else { return nil }
let currentDrawsInfo = Games.Info.CurrentDrawsInfo(currntDraw: currentDraws)
let gameInfo = Games.Info(gameId: gameId, bonusMultiplier: bonusMultiplier, maxTicketCost: maxTicketCost, currentDraws: currentDrawsInfo)
return gameInfo
})
// Инициализируем старую модельку
let games = Games(info: infos)
guard let response = games as? Response else {
completion(.failure(.decodingError))
return
}
completion(.success(response))
case .failure(let error):
…
}
}
}В итоге мы получаем готовую, старую модель, полученную из GraphQL.
Scalar type
В документации GraphQL скалярный тип описан следующим образом: «Скалярный тип представляет собой уникальный идентификатор, часто используемый для повторной выборки объекта или в качестве ключа для кэша». Для swift скалярный тип можно легко ассоциировать с typealias.
При написании клиента, я столкнулся с проблемой, что в моей схеме присутствовал скалярный тип Long, который по сути был
typealias Long = Int64 Всё бы ничего, но Apollo автоматически приводила все пользовательские скаляры к типу String, что вызывало креш. Проблему я решил так:
- Дописал в скрипте кодгена –passthroughCustomScalars
- Создал отдельный файл для typealias
- Добавил в файл
public typealias Long = Int64
В дальнейшем для каждого ��калярного типа нужно добавлять новый typealias. С версией Apollo 14.0 добавили «поддержку для Int кастомных скаляров». Если вы хотите избежать использования данного подхода и флага в кодгене, то ознакомьтесь с решением данной проблемы в гите Apollo.
Плюсы и минусы
Чем хорош такой подход? Достаточно быстрый переход на использование GraphQL, относительно подхода с убиранием всех старых моделей.
В дальнейшем, когда нам нужно будет получить данные для какого-либо экрана/модели мы можем обратиться к GraphQLClient и получить необходимые данные.
Из минусов:
- Преобразование типа модели в клиенте, можно было бы передать другой сущности т.к. работа с данными не входит в ответственность клиента.
- Разрастающийся GraphQLClient. После добавления новых запросов наш класс будет разрастаться всё больше и больше. Одно из решений — расширения в которых будут описаны методы загрузки, про них я рассказывал в главе GraphQLClient.
Выводы
В целом переход на GraphQL может оказаться быстрым и безболезненным при хорошо написанном клиенте. У меня он занял около трех дней = 24 рабочих часа. За это время я успел создать клиент, перевести модель и воссоздать модель GameInfo. Описанный подход — не панацея, а сугубо мое решение, реализованное в сжатые сроки.
Если среди вас есть гуру GraphQL, предлагаю поделиться опытом и рассказать насколько удобно использовать GraphQL + Apollo в больших проектах. Или овчинка выделки не стоит?
Спасибо за внимание!
