¿Cómo gestiona el código base subyacente para una API versionada?

104

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 forenamey separados en surnamelugar de un solo namecampo. (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.

Dylan Beattie
fuente
41
¡Gracias por hacer esta pregunta! ¡¡NO PUEDO creer que más personas no estén respondiendo esta pregunta !! Estoy harto y cansado de que todos tengan una opinión sobre cómo las versiones ingresan a un sistema, pero nadie parece abordar el problema real de enviar versiones a su código apropiado. A estas alturas debería haber al menos una serie de "patrones" o "soluciones" aceptadas para este problema aparentemente común. Hay una gran cantidad de preguntas sobre SO con respecto al "control de versiones de API". ¡Decidir cómo aceptar versiones es FRIKKIN SIMPLE (relativamente)! Manejarlo en el código base una vez que ingresa, ¡es DIFÍCIL!
arijeet

Respuestas:

45

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:

  • Un número reducido de cambios, cambios de baja complejidad o programa de cambios de baja frecuencia
  • Cambios que son en gran parte ortogonales al resto de la base de código: la API pública puede existir pacíficamente con el resto de la pila sin requerir una ramificación "excesiva" (para cualquier definición de ese término que elija adoptar) en el código.

No me resultó demasiado difícil eliminar las versiones obsoletas con este modelo:

  • Una buena cobertura de prueba significó que la extracción de una API retirada y el código de respaldo asociado aseguró que no hubo regresiones (bueno, mínimas)
  • Una buena estrategia de nomenclatura (nombres de paquetes con versiones de API, o versiones de API algo más desagradables en los nombres de métodos) facilitó la localización del código relevante
  • Las preocupaciones transversales son más difíciles; Las modificaciones a los sistemas de backend centrales para admitir múltiples API deben sopesarse con mucho cuidado. En algún momento, el costo del control de versiones del backend (ver el comentario sobre "excesivo" arriba) supera el beneficio de una única base de código.

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 GETrecurso ted teóricamente idempotente .

Me interesaría escuchar tu eventual decisión.

Palpatim
fuente
5
Solo por curiosidad, en el código fuente, ¿duplica los modelos entre v0 y v1 que no cambiaron? ¿O tienes v1 usando algunos modelos v0? Para mí, estaría confundido si viera v1 usando modelos v0 para algunos campos. Pero, por otro lado, reduciría el exceso de código. Para manejar múltiples versiones, ¿solo tenemos que aceptar y vivir con código duplicado para modelos que nunca cambiaron?
EdgeCaseBerg
1
Lo que recuerdo es que nuestros modelos de código fuente versionados independientemente de la propia API, por lo que, por ejemplo, API v1 podría usar el Modelo V1, y API v2 también podría usar el Modelo V1. Básicamente, el gráfico de dependencia interna para la API pública incluía tanto el código API expuesto como el código de "cumplimiento" de backend, como el servidor y el código modelo. Para múltiples versiones, la única estrategia que he usado es la duplicación de toda la pila: un enfoque híbrido (el módulo A está duplicado, el módulo B está versionado ...) parece muy confuso. YMMV por supuesto. :)
Palpatim
2
No estoy seguro de seguir lo que se sugiere para el tercer enfoque. ¿Hay ejemplos públicos de código estructurado así?
Ehtesh Choudhury
13

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.

S. Stavreva
fuente
Es difícil garantizar la compatibilidad si ha cambiado toda la base del código subyacente. Es mucho más seguro conservar la base de código anterior para las versiones de corrección de errores.
Marcelo Cantos
5

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 peor if(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.

edmarisov
fuente
Estoy pensando en probar en una única base de código en este momento. Mencionaste que las pruebas siempre tendrían que ser ramificadas, pero estoy pensando que todas las pruebas para v1, v2, v3, etc. también podrían vivir en la misma solución y ejecutarse todas al mismo tiempo. Estoy pensando en la decoración de las pruebas con atributos que especifican qué versiones apoyan: por ejemplo [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?
Lee Gunn
1
Bueno, después de 3 años aprendí que no hay una respuesta precisa a la pregunta original: D. Depende mucho del proyecto. Si puede permitirse congelar la API y solo mantenerla (por ejemplo, correcciones de errores), entonces seguiría ramificando / desconectando el código relacionado (lógica empresarial relacionada con la API + pruebas + punto final de descanso) y tendría todas las cosas compartidas en una biblioteca separada (con sus propias pruebas ). Si V1 va a coexistir con V2 durante bastante tiempo y el trabajo de funciones aún está en curso, los mantendría juntos y también las pruebas (cubriendo V1, V2, etc. y nombradas en consecuencia).
edmarisov
1
Gracias. Sí, parece ser un espacio bastante obstinado. Primero probaré el enfoque de una solución y veré cómo funciona.
Lee Gunn
0

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.

usuario1537847
fuente