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

105

Я читал о стратегиях управления версиями для API-интерфейсов ReST, и кое-что из них, похоже, не решает, как управлять базовой кодовой базой.

Скажем , мы делаем кучу ломки изменений в API - например, изменяя наш ресурс клиента так , чтобы он возвращал отдельным 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-интерфейсы в нескольких версиях, решают эту проблему.

Дилан Битти
источник
41
Спасибо, что задали этот вопрос! Я НЕ МОГУ поверить, что больше людей не отвечают на этот вопрос !! Я устал от того, что у всех есть мнение о том, как версии попадают в систему, но, похоже, никто не решает настоящую сложную проблему отправки версий в соответствующий код. К настоящему времени должен быть по крайней мере набор общепринятых «шаблонов» или «решений» этой, казалось бы, общей проблемы. Существует безумное количество вопросов по SO относительно «управления версиями API». Решить, как принимать версии, ФРИККИН ПРОСТО (относительно)! Обработать его в базе кода, как только он попадает, - ТРУДНО!
arijeet

Ответы:

45

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

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

Я не нашел слишком сложным удалить устаревшие версии с помощью этой модели:

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

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

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

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

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

Пальпатим
источник
5
Просто любопытно, в исходном коде вы дублируете модели между v0 и v1, которые не изменились? Или у вас v1 используете какие-то модели v0? Я был бы сбит с толку, если бы увидел v1, использующий модели v0 для некоторых полей. Но с другой стороны, это уменьшило бы раздувание кода. Для обработки нескольких версий нам просто нужно принять и жить с дублирующимся кодом для моделей, которые никогда не менялись?
EdgeCaseBerg
1
Я припоминаю, что наши модели с версиями исходного кода независимо от самого API, поэтому, например, API v1 может использовать модель V1, а API v2 также может использовать модель V1. По сути, внутренний граф зависимостей для общедоступного API включал в себя как открытый код API, так и внутренний код «выполнения», такой как код сервера и модели. Для нескольких версий единственная стратегия, которую я когда-либо использовал, - это дублирование всего стека - гибридный подход (модуль A дублируется, модуль B версируется ...) кажется очень запутанным. YMMV конечно. :)
Palpatim
2
Я не уверен, что следую тому, что предлагается для третьего подхода. Есть ли публичные примеры кода с подобной структурой?
Ehtesh Choudhury
13

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

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

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

Например:

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

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

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

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

С.Ставрева
источник
Трудно гарантировать совместимость, если изменилась вся базовая кодовая база. Намного безопаснее сохранить старую кодовую базу для выпусков с исправлениями ошибок.
Марсело Кантос
5

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

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

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

Эдмарисов
источник
Сейчас я думаю о тестировании в единой кодовой базе. Вы упомянули, что тесты всегда должны быть разветвленными, но я думаю, что все тесты для v1, v2, v3 и т. Д. Могут также работать в одном решении и запускаться одновременно. Я имею в виду декорирования тесты с атрибутами , которые указывают , какие версии они поддерживают: например [Version(From="v1", To="v2")], [Version(From="v2", To="v3")], [Version(From="v1")] // All versions просто исследуя сейчас, когда - либо слышал , чтобы кто же это?
Ли Ганн
1
Что ж, через 3 года я узнал, что на исходный вопрос нет точного ответа: D. Это очень зависит от проекта. Если вы можете позволить себе заморозить API и поддерживать его только (например, исправления ошибок), тогда я бы все равно разделил / отсоединил связанный код (бизнес-логика, связанная с API + тесты + конечная точка отдыха) и все общие материалы были бы в отдельной библиотеке (со своими собственными тестами. ). Если V1 будет сосуществовать с V2 в течение некоторого времени, а работа над функциями все еще продолжается, я бы оставил их вместе и вместе с тестами (охватывающий V1, V2 и т. Д. И названный соответственно).
edmarisov
1
Спасибо. Да, кажется, это довольно самоуверенное место. Я собираюсь сначала попробовать подход с одним решением и посмотреть, как он пойдет.
Ли Ганн
0

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

user1537847
источник