Интеграции API: Классы клиентов и обработка ошибок

То, как клиенты API сигнализируют о результатах и ошибках, имеет решающее значение для качества интеграции, особенно для недокументированных API.

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

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

Постановка проблемы

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

  • Недоступность из-за перебоев в работе сети или сервиса.
  • Неожиданные результаты или ошибки, поскольку существует множество других вещей, которые могут пойти не так: неправильные HTTP-заголовки, неправильная кодировка, недействительная полезная нагрузка и многое другое.

Следовательно, обработка и восстановление ошибок является центральным элементом качественной интеграции API. Мы рассмотрим четыре подхода, от привычных (исключения) до экзотических (монады результатов).

Метод 1: Исключения

Наиболее знакомый подход — это использование возвращаемого значения при успехе и вызов исключения при ошибке. Такое поведение широко распространено в Ruby и других языках. Простота и привычность — его преимущества:

  • Все знают, как поднимать исключения, что делает код клиентского класса легким для чтения и написания.
  • Все знают, как спасать исключения, что делает код обработки ошибок простым для написания (но не обязательно для чтения).
  • Необработанные ошибки не могут остаться незамеченными, поскольку необработанные исключения приведут к ошибке сервера.

К сожалению, есть и тонкие недостатки:

  • поток управления на основе исключений неидиоматичен и менее читабелен.
  • Исключения, вызванные ошибками API и внутренними ошибками клиентского класса, объединяются вместе; для их различения требуется повышенная осторожность.
  • При работе в консоли исключения, вызванные ошибками API, необходимо спасать и присваивать переменной для дальнейшей проверки.

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

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

def show
  begin
    weather = $weather.find_by_name(params[:name])
  rescue WhatWeather::ServiceError
    # If the API returns an error then return a 200 and an error message.
    # Exceptions unrelated to the API will propagate up the stack and likely
    # cause an internal server error (as they should!).
    render "unavailable"
    return
  end

  # Happy path continues here
end
Войти в полноэкранный режим Выйти из полноэкранного режима

Это подводит нас к следующему методу, который устраняет эти недостатки элегантно и идиоматично.

Метод 2: Кортежи и сопоставление шаблонов

Альтернативой, вдохновленной Go, является возврат [:ok, result] при успехе и [:error, details] при ошибке, с исключениями, оставленными для действительно исключительных ситуаций, таких как непоследовательное внутреннее состояние клиента.

Этот метод в сочетании с сопоставлением шаблонов, которое появилось в Ruby 2.7, приводит к идиоматическому и читаемому потоку управления. Приведенный ниже фрагмент вызывает воображаемый API What Weather и использует сопоставление шаблонов, чтобы решить, что делать дальше:

def show
  case $weather.find_by_name(params[:name])
  in [:ok, result]
    @weather = result.current_weather
  in [:error, :timeout] | [:error, :unavailable]
    render "unavailable"
    return
  end

  # Continue with the happy path.
end
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Во-первых, если возвращаемое клиентом значение не совпадает ни с одним случаем, то будет вызвана ошибка NoMatchingPatternError. Это полезно для выявления всех ошибок, встречающихся в природе, но может быть слишком радикальным. В таком случае достаточно общего шаблона [:error, _] и лог-записи с подробностями ошибки.

Во-вторых, иногда предпочтительнее вызывать исключение при ошибках API. В таких случаях клиентский класс может предложить bang-методы, например #find_by_name!, которые вызывают не-bang версию под капотом, но вызывают исключение при ошибке.

Очевидно, что все недостатки метода 1 были устранены, но появился новый: если вызов API не обернут в case, то выполнение будет продолжаться по счастливому пути даже при ошибках. К счастью, эта проблема может быть решена с помощью статического анализа кода, что приводит нас к третьему методу.

Метод 3: Кортежи, сопоставление шаблонов и линтинг

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

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

Если создание пользовательского правила linter неосуществимо (на самом деле, это не так сложно, как кажется), то импровизированный подход, основанный на grep, может быть достаточно хорош. Например, обнаружение всех вызовов метода $weather, которые не обернуты в case, может быть выполнено с помощью grep -r '$weather.' . | grep -v 'case $weather'.

Метод 4: Монады результата

Монады заслуживают отдельной статьи, поэтому мы кратко опишем их и будем использовать dry-monads в последующих примерах.

Монада результата напоминает полиморфную реализацию булевых чисел с более сложным интерфейсом. Существует два типа результатов: успехи и неудачи. Успех обертывает результат вычисления (в нашем случае вызов API); неудача обертывает описание ошибки. Результат может быть преобразован в другой результат с помощью метода #bind. Когда этот метод вызывается при успешном результате, он передает обернутое значение в блок и возвращает другой результат (успех или неудачу). Когда она вызывается при неудаче, она ничего не делает и позволяет этой неудаче распространяться дальше. В этом есть сходство с логикой замыкания булевых операторов.

Монады-результаты проявляют себя, когда требуется композиция, поэтому давайте усовершенствуем наше действие контроллера: оно определяет наше местоположение с помощью основанного на монаде сервиса $geolocation и затем возвращает погоду в нашем местоположении. Основанная на монаде версия клиентского класса API погоды могла бы выглядеть так, как показано в следующем фрагменте:

class WeatherService::WhatWeather
  # Required to access monad-specific features.
  extend Dry::Monads[:result]

  def find_by_name(name)
    result = http_client.get(
      "/api/weather",
      params: { name: name },
      headers: { "X-API-Key": api_key }
    )

    if result.status == 200
      # Build the weather object and wrap it in a success monad.
      Success(build_weather(result))
    else
      # Extract error details from the response and wrap it in a failure monad.
      Failure(result.json)
    end
  end
end
Вход в полноэкранный режим Выйти из полноэкранного режима

Действие контроллера будет использовать $geolocation.locate_request, а затем $weather.find_by_location. Оба метода возвращают монады результатов, которые могут быть объединены с помощью #bind:

def show
  weather =
    # locate_request returns either a success or failure.
    $geolocation.locate_request(request).bind do |location|
      # #bind on a success unwraps the value and processes them further via this
      # block. $weather.find_by_location returns another result which becomes
      # the result of the whole computation.
      #
      # However, if geolocation fails then #bind will be called on a failure and
      # would NOT call this block. It'd simply return the original failure.
      $weather.find_by_location(location)
    end

  # weather is a result monad, NOT the actual value.
  if weather.success?
    # Forcibly extract the weather value.
    @weather = weather.value!
  else
    render "unavailable"
  end
end
Вход в полноэкранный режим Выйти из полноэкранного режима

Монады могут показаться непривычными и потребовать некоторого времени для изучения. Однако частое использование монад в составе операций (не только вызовов API сторонних разработчиков) может сделать их стоящим вложением.

Заключение

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

Эта статья первоначально появилась на моем сайте.

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