Так или иначе, каждый разработчик вступил в контакт с API . Либо интеграция крупной системы для большой корпорации, создание некоторых модных диаграмм с новейшей библиотекой графов, либо просто взаимодействие с его любимым языком программирования. Правда в том, что API есть везде! Они на самом деле представляют собой фундаментальный строительный блок современного Интернета, играя фундаментальную роль в процессе обмена данными, который происходит между различными системами и устройствами. От простого погодного виджета на вашем мобильном телефоне до оплаты кредитной картой, которую вы выполняете в онлайн-магазине, все это было бы невозможно, если бы эти системы не связывались друг с другом, вызывая API-интерфейсы друг друга.
Таким образом, с постоянно растущей экосистемой разнородных устройств, подключенных к Интернету, API ставят новые сложные задачи. Хотя они должны продолжать работать надежно и безопасно, они также должны быть совместимы со всеми этими устройствами, которые могут варьироваться от наручных часов до самого современного сервера в дата-центре.
ОТДЫХ на помощь
Одной из наиболее широко используемых технологий для создания таких API являются так называемые REST API. Эти API призваны обеспечить общий и стандартизированный способ связи между гетерогенными системами. Поскольку они в значительной степени зависят от стандартных протоколов связи и представления данных, таких как HTTP, XML или JSON, довольно легко обеспечить реализацию на стороне клиента для большинства языков программирования, что делает их совместимыми с подавляющим большинством систем и устройств.
Таким образом, хотя эти REST API могут быть совместимы с большинством устройств и технологий, они также должны развиваться. И проблема эволюции в том, что вам иногда приходится поддерживать ретро-совместимость со старыми версиями клиента.
Давайте создадим пример.
Давайте представим систему встреч, в которой у вас есть API для создания и получения встреч. Для упрощения давайте представим объект нашего назначения с датой и именем гостя. Что-то вроде этого:
1
2
3
4
5
|
public class AppointmentDTO { public Long id; public Date date; public String guestName; } |
Очень простой REST API будет выглядеть так:
01
02
03
04
05
06
07
08
09
10
11
|
@Path ( "/api/appointments" ) public class AppointmentsAPI { @GET @Path ( "/{id}" ) public AppointmentDTO getAppointment( @PathParam ( "id" ) String id) { ... } @POST public void createAppointment(AppointmentDTO appointment) { ... } } |
Давайте предположим, что этот простой простой API работает и используется на мобильных телефонах, планшетах и различных веб-сайтах, которые позволяют бронировать и отображать встречи. Все идет нормально.
В какой-то момент вы решаете, что было бы очень интересно начать собирать статистику о вашей системе назначений. Для простоты вы просто хотите знать, кто является человеком, который забронировал номер чаще всего. Для этого вам необходимо сопоставить гостя между собой и решить, что вам нужно добавить уникальный идентификатор для каждого гостя. Давайте использовать электронную почту. Теперь ваша объектная модель будет выглядеть примерно так:
01
02
03
04
05
06
07
08
09
10
|
public class AppointmentDTO { public Long id; public Date date; public GuestDTO guest; } public class GuestDTO { public String email; public String name; } |
Таким образом, наша объектная модель немного изменилась, что означает, что нам придется адаптировать бизнес-логику на нашем API.
Проблема
Хотя адаптация API для хранения и извлечения новых типов объектов должна быть легкой задачей, проблема заключается в том, что все ваши текущие клиенты используют старую модель и будут продолжать это делать до тех пор, пока не обновятся. Можно утверждать, что вам не нужно беспокоиться об этом, и что клиенты должны обновиться до более новой версии, но правда в том, что вы не можете на самом деле форсировать обновление с ночи на день. Всегда будет временное окно, в котором вы должны поддерживать обе модели, что означает, что ваш API должен быть ретро-совместимым.
Это где ваши проблемы начинаются.
Итак, вернемся к нашему примеру, в данном случае это означает, что нашему API придется обрабатывать обе объектные модели и иметь возможность хранить и извлекать эти модели в зависимости от клиента. Итак, давайте добавим guestName к нашему объекту, чтобы сохранить совместимость со старыми клиентами:
1
2
3
4
5
6
7
8
9
|
public class AppointmentDTO { public Long id; public Date date; @Deprecated //For retro compatibility purposes public String guestName; public GuestDTO guest; } |
Помните, что хорошее правило для объектов API заключается в том, что вы никогда не должны удалять поля. Добавление новых обычно не нарушает реализацию клиента (при условии, что они следуют хорошему правилу большого пальца игнорирования новых полей), но удаление полей обычно ведет к кошмарам.
Теперь для обеспечения совместимости API есть несколько различных вариантов. Давайте посмотрим на некоторые из альтернатив:
- Дублирование : чисто и просто. Создайте новый метод для новых клиентов и используйте старые, используя тот же самый.
- Параметры запроса : введите флаг для управления поведением. Что-то вроде useGasts = true.
- Управление версиями API : введите версию в своем URL-пути, чтобы указать, какую версию метода вызывать.
Так что все эти альтернативы имеют свои плюсы и минусы. Хотя дублирование может быть простым, оно может легко превратить ваши классы API в чашу дублированного кода.
Параметры запроса могут (и должны) использоваться для контроля поведения (например, для добавления нумерации страниц к списку), но мы должны избегать их использования для реальных изменений API, поскольку они обычно имеют постоянный характер, и поэтому вы не хотите делать это необязательно для потребителя.
Управление версиями кажется хорошей идеей. Он обеспечивает чистый способ развития API, сохраняет старые клиенты отделенными от новых и предоставляет общую основу для всех видов изменений, которые произойдут в течение срока службы API. С другой стороны, это также вносит некоторую сложность, особенно если у вас будут разные звонки в разных версиях. Вашим клиентам в конечном итоге придется самостоятельно управлять развитием API, обновляя вызов вместо API. Это похоже на то, что вместо обновления библиотеки до следующей версии вы бы обновили только определенный класс этой библиотеки. Это может легко превратиться в версию кошмара …
Чтобы преодолеть это, мы должны убедиться, что наши версии охватывают весь API. Это означает, что я должен иметь возможность вызывать каждый доступный метод в / v1, используя / v2. Конечно, если в v2 существует более новая версия данного метода, его следует запускать при вызове / v2. Однако, если данный метод не изменился в v2, я ожидаю, что версия v1 будет без проблем вызываться.
Управление версиями API на основе наследования
Чтобы достичь этого, мы можем воспользоваться преимуществами полиморфных возможностей объектов Java. Мы можем создавать версии API иерархическим образом, чтобы более старые методы версий могли быть переопределены более новыми, а вызовы более новой версии неизмененного метода могут быть легко перенесены на более раннюю версию.
Итак, вернемся к нашему примеру, мы могли бы создать новую версию метода create, чтобы API выглядел следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Path ( "/api/v1/appointments" ) //We add a version to our base path public class AppointmentsAPIv1 { //We add the version to our API classes @GET @Path ( "/{id}" ) public AppointmentDTO getAppointment( @PathParam ( "id" ) String id) { ... } @POST public void createAppointment(AppointmentDTO appointment) { //Your old way of creating Appointments only with names } } //New API class that extends the previous version @Path ( "/api/v2/appointments" ) public class AppointmentsAPIv2 extends AppointmentsAPIv1 { @POST @Override public void createAppointment(AppointmentDTO appointment) { //Your new way of creating appointments with guests } } |
Итак, теперь у нас есть 2 рабочие версии нашего API. В то время как все старые клиенты, которые еще не обновились до новой версии, будут продолжать использовать v1 — и не увидят изменений — все ваши новые потребители теперь могут использовать последнюю версию v2. Обратите внимание, что все эти звонки действительны:
Вызов | Результат |
---|---|
GET /api/v1/appointments/123 |
Будет запускать getAppointment в классе v1 |
GET /api/v2/appointments/123 |
Будет запускать getAppointment в классе v1 |
POST /api/v1/appointments |
Будет запускать createAppointment в классе v1 |
POST /api/v2/appointments |
Будет запускать createAppointment в классе v2 |
Таким образом, любые потребители, которые хотят начать использовать последнюю версию, должны будут только обновить свои базовые URL-адреса до соответствующей версии, и весь API будет плавно переходить на самые последние реализации, сохраняя старые неизменные.
Предостережение
Для острого глаза есть немедленное предупреждение с этим подходом. Если ваш API состоит из десятых долей различных классов, более новая версия будет подразумевать дублирование их всех в верхнюю версию, даже если у вас нет никаких изменений. Это немного кода котельной пластины, который в основном может генерироваться автоматически. Все еще раздражает все же.
Хотя нет быстрого способа преодолеть это, использование интерфейсов может помочь. Вместо создания нового класса реализации вы можете просто создать новый интерфейс с аннотацией Path и внедрить его в текущий класс реализации. Хотя вам придется создать один интерфейс для каждого класса API, это немного чище. Это немного помогает, но это все же предостережение.
Последние мысли
Управление версиями API, похоже, является актуальной темой. Существует множество различных точек зрения и мнений, но, похоже, не хватает стандартных лучших практик. Хотя этот пост не ставит своей целью обеспечить это, я надеюсь, что он поможет улучшить структуру API и повысить его удобство обслуживания.
И последнее слово Роберту Кортесу за то, что он поддержал этот пост в своем блоге. На самом деле это мой первый пост в блоге, поэтому загружайте пушки и стреляйте по желанию. 🙂
Ссылка: | REST API Evolution от нашего партнера JCG Децио Соузы в блоге Java-блога Роберто Кортеса . |