Интеграции API: Создание клиентских классов

Код говорит громче слов, поэтому мы рассмотрим пример интеграции с погодным сервисом WhatWeather, который возвращает текущие погодные условия по широте и долготе или названию города. Давайте вкратце опишем этот воображаемый сервис, а затем приступим к интеграции.

Обзор API WhatWeather

API WhatWeather доступен по HTTP и предлагает одну конечную точку, которую можно использовать двумя способами:

API отвечает 200 OK и объектом JSON, содержащим два ключа: "температура" и "влажность". Аутентификация обрабатывается с помощью маркера, передаваемого в пользовательском заголовке X-API-Key. Любой другой код состояния, даже в диапазоне 200-299, может быть принят за ошибку.

Шаг 1: Создание класса клиента

Сначала мы должны определить интерфейс клиентского класса. Несмотря на то, что API WhatWeather предлагает одну конечную точку, имеет смысл иметь два отдельных метода: #weather_by_coordinates и #weather_by_name. Мы должны стремиться к максимальной ясности кода, а не к зеркальному отражению структуры базового API.

Во-вторых, нам нужно пространство имен (модуль Ruby) для всего кода, связанного с погодой. Его имя не должно ссылаться на конкретного поставщика API, чтобы сохранить общность кода и избежать привязки к конкретному поставщику. WeatherService кажется разумным названием.

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

WeatherService::Weather = Struct.new(:temperature, :humidity)
Войти в полноэкранный режим Выйти из полноэкранного режима

В более сложных случаях может потребоваться собственный класс. Важно сделать их независимыми от фактического поставщика API — иначе он может превратиться в спрута, щупальца которого дотягиваются до самых глубин приложения. Например, клиентский класс должен анализировать и преобразовывать HTTP-ответы в экземпляры WeatherService::Weather и подобных классов, чтобы в случае изменения их API в нашей кодовой базе было только одно место, которое нужно адаптировать.

Мы готовы к четвертому и последнему шагу: реализации реального класса клиента. Мы можем использовать сторонний клиентский гем или взаимодействовать с API через HTTP, GraphQL или другой протокол. Какой бы метод мы ни использовали, важно помнить о предыдущем пункте — не позволяйте фактической реализации клиента просочиться через клиентский класс и наводнить остальной код.

Приведенный ниже пример клиента принимает объект HTTP-клиента через конструктор. Имейте в виду, что он не готов к производству, поскольку его валидация ввода, обработка ошибок и разбор ответа являются рудиментарными, но этого достаточно для иллюстрации идеи.

class WeatherService::WhatWeather
  def initialize(http_client:, api_key:)
    @http_client = http_client
    @api_key = api_key
  end

  def weather_by_coordinates(coordinates)
    result = http_client.get(
      "/api/weather",
      params: { lat: coordinates[0], lon: coordinates[1] },
      headers: { "X-API-Key": api_key }
    )
    return [:error, reason: :invalid_response] if result.status != 200

    build_weather(result)
  end

  def weather_by_name(name)
    result = http_client.get(
      "/api/weather",
      params: { name: name },
      headers: { "X-API-Key": api_key }
    )
    return [:error, reason: :invalid_response] if result.status != 200

    build_weather(result)
  end

  private

  attr_reader :http_client, :api_key

  def build_weather(result)
    [
      :ok,
      WeatherService::Weather(
        temperature: result.json.fetch("temperature"),
        humidity: result.json.fetch("humidity")
      )
    ]
  rescue KeyError
    [:error, reason: :invalid_response]
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

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

Шаг 2: Интеграция клиента в приложение

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

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

Раньше я полагался на атрибуты модуля (например, WeatherService.client), но теперь перешел на использование реальных глобальных переменных, например, $weather_service. Я считаю, что это намного чище, чем фактический глобал, маскирующийся под вызов метода.

Лучшее место для инстанцирования клиента — инициализатор Rails. Клиент погодного сервиса может быть инстанцирован в config/initializers/weather_service.rb:

Rails.application.config.to_prepare do
  $weather_service = WeatherService::WhatWeather.new(
    http_client: HttpClient.new,
    api_key: ENV.fetch("WHATEVER_WEATHER_API_KEY")
  )
end
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем использовать $weather_service всякий раз, когда нам нужно сделать вызов API. Однако это еще не конец истории, поскольку есть преимущества, которые можно получить при тестировании и разработке.

Шаг 3: Тестирование

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

Первым шагом является имитация клиента перед каждым тестовым примером и его последующая очистка. Ниже мы используем MiniTest и Minitest::Mock, но та же идея применима и к другим тестовым и мокинговым фреймворкам.

class ActiveSupport::TestCase
  setup do
    @old_weather_service = $weather_service
    $weather_service = Minitest::Mock.new
  end

  teardown do
    $weather_service.verify
    $weather_service = @old_weather_service
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

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

$weather_service.mock(
  :weather_by_name,
  WeatherService::Result.new(temperature: 80, humidity: 63),
  ["New York City"]
)
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Давайте обратим внимание на то, какую пользу этот подход может принести при разработке.

Шаг 4: Разработка

Взаимодействие с приложением является важным компонентом цикла обратной связи при разработке. Однако использование реального API в процессе разработки может затруднить изучение крайних случаев (маловероятных возвращаемых значений или ошибок). Например, мы не можем заставить WhatWeather лгать о температуре в Нью-Йорке. Это еще одна ситуация, в которой наш подход может пригодиться.

Решением является специфический для разработки клиентский класс, возвращающий заглушенные ответы. Например:

def weather_by_name(name)
  case name
  in "New York City" then Result.new(temperature: 5, humidity: 50)
  else Result.new(temperature: 65, humidity: 47)
  end
end
Войти в полноэкранный режим Выход из полноэкранного режима

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

Переключение между реальным и заглушкой клиента API может быть вопросом конфигурации. Например, файл инициализатора клиента может быть изменен следующим образом:

Rails.application.config.to_prepare do
  case ENV.fetch("WHATWEATHER_CLIENT", "production")
  in "production"
    $weather_service = WeatherService::WhatWeather.new(
      http_client: HttpClient.new,
      api_key: ENV.fetch("WHATEVER_WEATHER_API_KEY")
    )
  in "development"
    $weather_service = WeatherService::StubWhatWeather.new
  end
end
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь клиент можно выбрать, установив WHATWEATHER_CLIENT в "production" или "development".

Дальнейшие шаги

Мы описали высокоуровневый подход, но пропустили многие детали реализации, которые должны быть рассмотрены в реальной производственной системе. Например:

  • Установление шаблонов конфигурации клиента. Какие переменные необходимы? Как мы будем конфигурировать основной и производственный клиенты?
  • Обеспечение простоты переключения между основными и производственными клиентами.
  • Валидация ввода клиента, разбор ответа и отчет об ошибках.
  • Создание набора специализированных клиентских тестов, использующих реальный API для обеспечения корректной работы клиента.
  • Ведение нескольких клиентов (или объектов сервиса в целом) и их взаимоотношений.

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

  • Глобальное состояние делает возможным использование клиента из любого места без явной зависимости. Легко использовать клиента из модели, что, в общем, должно быть нежелательно.
  • Сложные интеграции, такие как Stripe, могут потребовать много пользовательского кода разбора и преобразования результатов. Возможно, имеет смысл создать некий DSL для выполнения этой задачи, что увеличит сложность реализации, или пустить ее через клиент в остальную часть приложения, что увеличит сопряжение. Это может быть непростой выбор.
  • Издевательство над API может привести к ситуации, когда набор тестов будет «зеленым», а производство — нет.

Заключительные размышления

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

Если вам понравилось читать эту статью, то я рекомендую вам взглянуть на другие статьи на моем сайте.

Оцените статью
devanswers.ru
Добавить комментарий