He estado leyendo sobre las estrategias de control de versiones para las API de ReST, y algo que ninguno de ellos parece abordar es cómo administra el código base subyacente.
Digamos que estamos haciendo un montón de cambios importantes en una API, por ejemplo, cambiando nuestro recurso Cliente para que devuelva campos forename
y separados en surname
lugar de un solo name
campo. (Para este ejemplo, usaré la solución de control de versiones de URL ya que es fácil comprender los conceptos involucrados, pero la pregunta es igualmente aplicable a la negociación de contenido o encabezados HTTP personalizados)
Ahora tenemos un punto final en http://api.mycompany.com/v1/customers/{id}
y otro punto final incompatible en http://api.mycompany.com/v2/customers/{id}
. Todavía estamos lanzando correcciones de errores y actualizaciones de seguridad para la API v1, pero el desarrollo de nuevas funciones ahora se centra en v2. ¿Cómo escribimos, probamos e implementamos cambios en nuestro servidor API? Puedo ver al menos dos soluciones:
Utilice una rama / etiqueta de control de fuente para la base de código v1. v1 y v2 se desarrollan y se implementan de forma independiente, con fusiones de control de revisión que se utilizan según sea necesario para aplicar la misma corrección de errores a ambas versiones, similar a cómo administraría las bases de código para aplicaciones nativas al desarrollar una nueva versión importante sin dejar de ser compatible con la versión anterior.
Haga que la base de código conozca las versiones de API, de modo que termine con una única base de código que incluya tanto la representación del cliente v1 como la representación del cliente v2. Trate el control de versiones como parte de la arquitectura de su solución en lugar de un problema de implementación, probablemente usando alguna combinación de espacios de nombres y enrutamiento para asegurarse de que las solicitudes sean manejadas por la versión correcta.
La ventaja obvia del modelo de rama es que es trivial eliminar las versiones antiguas de la API, simplemente deje de implementar la rama / etiqueta apropiada, pero si está ejecutando varias versiones, podría terminar con una estructura de rama y una canalización de implementación realmente complicadas. El modelo de "base de código unificada" evita este problema, pero (¿creo?) Haría mucho más difícil eliminar los recursos y puntos finales obsoletos de la base de código cuando ya no sean necesarios. Sé que esto es probablemente subjetivo, ya que es poco probable que haya una respuesta correcta simple, pero tengo curiosidad por comprender cómo las organizaciones que mantienen API complejas en múltiples versiones están resolviendo este problema.
fuente
Respuestas:
He utilizado las dos estrategias que mencionas. De esos dos, prefiero el segundo enfoque, que es más simple, en casos de uso que lo respaldan. Es decir, si las necesidades de control de versiones son simples, opte por un diseño de software más simple:
No me resultó demasiado difícil eliminar las versiones obsoletas con este modelo:
El primer enfoque es ciertamente más simple desde el punto de vista de reducir el conflicto entre versiones coexistentes, pero la sobrecarga de mantener sistemas separados tendía a superar el beneficio de reducir el conflicto de versiones. Dicho esto, fue muy sencillo crear una nueva pila de API pública y comenzar a iterar en una rama de API separada. Por supuesto, la pérdida generacional se produjo casi de inmediato y las ramas se convirtieron en un lío de fusiones, fusiones de resolución de conflictos y otras cosas divertidas.
Un tercer enfoque se encuentra en la capa arquitectónica: adopte una variante del patrón Facade y abstraiga sus API en capas con versiones de cara al público que se comuniquen con la instancia de Facade adecuada, que a su vez se comunica con el backend a través de su propio conjunto de API. Your Facade (utilicé un adaptador en mi proyecto anterior) se convierte en su propio paquete, autónomo y comprobable, y le permite migrar las API frontend independientemente del backend y entre sí.
Esto funcionará si las versiones de su API tienden a exponer los mismos tipos de recursos, pero con diferentes representaciones estructurales, como en su ejemplo de nombre completo / nombre / apellido. Se vuelve un poco más difícil si comienzan a depender de diferentes cálculos de backend, como en "Mi servicio de backend ha devuelto un interés compuesto calculado incorrectamente que se ha expuesto en la API pública v1. Nuestros clientes ya han parcheado este comportamiento incorrecto. Por lo tanto, no puedo actualizar eso cálculo en el backend y hacer que se aplique hasta v2. Por lo tanto, ahora necesitamos bifurcar nuestro código de cálculo de intereses ". Afortunadamente, estos tienden a ser poco frecuentes: prácticamente hablando, los consumidores de API RESTful prefieren las representaciones de recursos precisas sobre la compatibilidad con versiones anteriores de error por error, incluso entre cambios constantes en un
GET
recurso ted teóricamente idempotente .Me interesaría escuchar tu eventual decisión.
fuente
Para mí, el segundo enfoque es mejor. Lo he usado para los servicios web SOAP y planeo usarlo también para REST.
Mientras escribe, el código base debe tener en cuenta la versión, pero se puede utilizar una capa de compatibilidad como capa separada. En su ejemplo, la base de código puede producir una representación de recursos (JSON o XML) con nombre y apellido, pero la capa de compatibilidad lo cambiará para tener solo el nombre.
El código base debería implementar solo la última versión, digamos v3. La capa de compatibilidad debe convertir las solicitudes y respuestas entre la versión más reciente v3 y las versiones compatibles, por ejemplo, v1 y v2. La capa de compatibilidad puede tener adaptadores separados para cada versión compatible que se pueden conectar como cadena.
Por ejemplo:
Solicitud del cliente v1: v1 adaptarse a v2 ---> v2 adaptarse a v3 ----> codebase
Solicitud de cliente v2: v1 adaptarse a v2 (omitir) ---> v2 adaptarse a v3 ----> base de código
Para la respuesta, los adaptadores funcionan simplemente en la dirección opuesta. Si está utilizando Java EE, puede utilizar la cadena de filtros de servlets como cadena de adaptadores, por ejemplo.
Eliminar una versión es fácil, elimine el adaptador correspondiente y el código de prueba.
fuente
La ramificación me parece mucho mejor y utilicé este enfoque en mi caso.
Sí, como ya mencionaste: las correcciones de errores de backporting requerirán algo de esfuerzo, pero al mismo tiempo, el soporte de múltiples versiones bajo una base de origen (con enrutamiento y todas las demás cosas) requerirá si no menos, pero al menos el mismo esfuerzo, haciendo que el sistema sea más complicado y monstruoso con diferentes ramas de la lógica en su interior (en algún punto del control de versiones, definitivamente llegará a
case()
señalar que los módulos de la versión tienen código duplicado o incluso peorif(version == 2) then...
). Además, no olvide que, para fines de regresión, aún debe mantener las pruebas ramificadas.Con respecto a la política de control de versiones: mantendría un máximo de -2 versiones de la actual, desaprobando el soporte para las antiguas, lo que daría algo de motivación a los usuarios para que se muevan.
fuente
[Version(From="v1", To="v2")]
,[Version(From="v2", To="v3")]
,[Version(From="v1")] // All versions
sólo para explorar ahora, nunca oído a nadie hacerlo?Por lo general, la introducción de una versión principal de la API que lo lleva a tener que mantener varias versiones es un evento que no ocurre (o no debería) ocurrir con mucha frecuencia. Sin embargo, no se puede evitar por completo. Creo que, en general, es una suposición segura que una versión principal, una vez introducida, permanecería como la última versión durante un período de tiempo relativamente largo. En base a esto, preferiría lograr simplicidad en el código a expensas de la duplicación, ya que me da más confianza de no romper la versión anterior cuando introduzco cambios en la última.
fuente