Как вы управляете базовой базой кода для API с версиями?

Я читал о стратегиях управления версиями API-интерфейсов REST, и что-то, о чем никто из них не обращается, - это то, как вы управляете базовой базой кода.

Предположим, что мы вносим некоторые изменения в API - например, меняем ресурс Customer, чтобы он возвращал отдельные поля forename и surname вместо одного поля name. (В этом примере я буду использовать решение для управления версиями URL-адресов, поскольку он легко понимает связанные с ним понятия, но вопрос в равной степени применим к согласованию контента или пользовательским HTTP-заголовкам)

Теперь мы имеем конечную точку в http://api.mycompany.com/v1/customers/{id} и другую несовместимую конечную точку в http://api.mycompany.com/v2/customers/{id}. Мы по-прежнему выпускаем исправления и обновления для системы безопасности для API v1, но теперь новая разработка функций сосредоточена на v2. Как мы пишем, тестируем и развертываем изменения на нашем сервере API? Я вижу как минимум два решения:

  • Используйте ветку/тег управления источника для кодовой базы v1. v1 и v2 разрабатываются и развертываются независимо друг от друга, при этом контроль версий сглаживается, если необходимо, для применения одного и того же исправления к обеим версиям - аналогично тому, как вы управляете кодовыми базами для родных приложений при разработке новой новой версии, сохраняя при этом предыдущую версию.

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

Очевидным преимуществом модели ветки является то, что тривиально удалять старые версии API - просто прекратите развертывание соответствующей ветки/тега, но если вы используете несколько версий, вы можете получить действительно запутанную структуру ветвей и развертывание трубопровод. Модель "унифицированная кодовая база" позволяет избежать этой проблемы, но (я думаю?) Было бы намного сложнее удалить устаревшие ресурсы и конечные точки из кодовой базы, когда они больше не требуются. Я знаю, что это, вероятно, субъективно, потому что вряд ли будет простой правильный ответ, но мне любопытно понять, как решить эту проблему организации, которые поддерживают сложные API-интерфейсы в нескольких версиях.

Ответ 1

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

  • Небольшое количество изменений, низкая сложность изменений или график с низкой частотой изменения.
  • Изменения, которые в значительной степени ортогональны остальной части кодовой базы: публичный API может существовать мирно с остальной частью стека, не требуя "чрезмерного" (для любого определения этого термина, который вы решите принять), разветвления в коде

Мне не посчастливилось удалить устаревшие версии с помощью этой модели:

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

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

Третий подход находится на архитектурном уровне: используйте вариант шаблона Facade и отрисуйте свои API-интерфейсы на публичные обрамленные версии, которые говорят с соответствующим экземпляром Facade, который, в свою очередь, обращается к бэкэнд через свой собственный набор API-интерфейсы. Ваш Фасад (я использовал Адаптер в моем предыдущем проекте) становится его собственным пакетом, самодостаточным и проверяемым, и позволяет вам переносить интерфейсные API независимо от бэкэнд и друг друга.

Это будет работать, если ваши версии API будут демонстрировать одни и те же ресурсы, но с различными структурными представлениями, например, в вашем имени fullname/forename/surname. Это становится немного сложнее, если они начинают полагаться на различные вычисления на основе бэкэнд, например, "Моя бэкэнд-служба вернула неверно рассчитанные сложные проценты, которые были выставлены в открытом API v1. Наши клиенты уже исправили это неправильное поведение. Поэтому я не могу обновить это вычисление в бэкэнд и применение его до версии 2. Поэтому нам теперь нужно развить наш код расчета процентов". К счастью, они имеют тенденцию быть нечастыми: практически говоря, потребители API RESTful предпочитают точное представление ресурсов по сравнению с обратной совместимостью с ошибкой для ошибок, даже среди неизменных изменений на теоретически идемпотентном ресурсе GET.

Мне будет интересно услышать ваше окончательное решение.

Ответ 2

Для меня второй подход лучше. Я использую его для веб-сервисов SOAP и планирую использовать его для REST.

Когда вы пишете, код должен быть осведомлен о версии, но уровень совместимости можно использовать как отдельный слой. В вашем примере база кода может создавать представление ресурсов (JSON или XML) с именем и фамилией, но уровень совместимости изменит его, чтобы вместо него было только имя.

Кодовая база должна реализовать только последнюю версию, скажем v3. Уровень совместимости должен преобразовывать запросы и ответ между последней версией v3 и поддерживаемыми версиями, например v1 и v2. Уровень совместимости может иметь отдельные адаптеры для каждой поддерживаемой версии, которые могут быть подключены как цепочка.

Например:

Клиент v1 запрос: v1 адаптироваться к v2 --- > v2 адаптироваться к v3 ---- > codebase

Клиент v2 запрос: v1 адаптироваться к v2 (пропустить) --- > v2 адаптироваться к v3 ---- > codebase

Для ответа адаптеры работают просто в противоположном направлении. Если вы используете Java EE, вы можете использовать цепочку фильтров сервлетов как цепочку адаптеров.

Удаление одной версии легко, удалите соответствующий адаптер и тестовый код.

Ответ 3

Ветвление кажется мне намного лучше, и я использовал этот подход в моем случае.

Да, как вы уже упоминали - исправления ошибок в backporting потребуют некоторых усилий, но в то же время поддержка нескольких версий под одной исходной базой (с маршрутизацией и всеми другими материалами) потребует вас, если не меньше, но, по крайней мере, делая систему более сложной и чудовищной с различными ветвями логики внутри (в какой-то момент версии вы наверняка придете к огромному case(), указывая на модули версии, имеющие дублированный код или еще хуже if(version == 2) then...). Также не забывайте, что для регрессионных целей вам все равно нужно держать тесты разветвленными.

Относительно политики управления версиями: я бы сохранил как max -2 версии от текущей, не одобряющей поддержки старых - это создало бы некоторую мотивацию для перемещения пользователей.