Pull to refresh

Как мы переводили легаси проект на GraphQL

Reading time5 min
Views4.1K
Привет, Хабр. Меня зовут Антон Потапов, я iOS разработчик в компании FINCH. Сегодня я хочу подробно рассказать про то как перевести мобильный проект на GraphQL, описать плюсы и минусы этого подхода. Приступим.

Краткое введение


«Легаси». Я думаю каждый слышал это страшное слово, а большинство встречались с ним лицом к лицу. Поэтому не нужно рассказывать насколько сложно интегрировать новые и большие функции в легаси проект.

image

Один из наших проектов — приложение для крупной лотерейной компании (точно название дать не могу так как 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 в больших проектах. Или овчинка выделки не стоит?

Спасибо за внимание!
Tags:
Hubs:
+11
Comments14

Articles