Comment gérez-vous la base de code sous-jacente pour une API versionnée?


105

J'ai lu des informations sur les stratégies de gestion des versions des API ReST, et aucune d'elles ne semble aborder la façon dont vous gérez la base de code sous-jacente.

Disons que nous apportons un tas de modifications de rupture à une API - par exemple, en modifiant notre ressource Client afin qu'elle renvoie des champs forenameet séparés surnameau lieu d'un seul namechamp. (Pour cet exemple, j'utiliserai la solution de gestion des versions d'URL car il est facile de comprendre les concepts impliqués, mais la question s'applique également à la négociation de contenu ou aux en-têtes HTTP personnalisés)

Nous avons maintenant un point de terminaison à http://api.mycompany.com/v1/customers/{id}, et un autre point de terminaison incompatible à http://api.mycompany.com/v2/customers/{id}. Nous publions toujours des corrections de bogues et des mises à jour de sécurité pour l'API v1, mais le développement de nouvelles fonctionnalités se concentre désormais sur la v2. Comment écrire, tester et déployer les modifications sur notre serveur API? Je peux voir au moins deux solutions:

  • Utilisez une branche / une balise de contrôle de source pour la base de code v1. v1 et v2 sont développés et déployés indépendamment, avec des fusions de contrôle de révision utilisées si nécessaire pour appliquer la même correction de bogue aux deux versions - de la même manière que vous gériez les bases de code pour les applications natives lors du développement d'une nouvelle version majeure tout en prenant en charge la version précédente.

  • Rendez la base de code elle-même consciente des versions de l'API, de sorte que vous vous retrouvez avec une seule base de code qui comprend à la fois la représentation client v1 et la représentation client v2. Traitez la gestion des versions comme faisant partie de l'architecture de votre solution plutôt que comme un problème de déploiement - probablement en utilisant une combinaison d'espaces de noms et de routage pour vous assurer que les demandes sont traitées par la version correcte.

L'avantage évident du modèle de branche est qu'il est facile de supprimer les anciennes versions d'API - arrêtez simplement de déployer la branche / balise appropriée - mais si vous exécutez plusieurs versions, vous pourriez vous retrouver avec une structure de branche et un pipeline de déploiement vraiment compliqués. Le modèle de «base de code unifiée» évite ce problème, mais (je pense?) Rendrait beaucoup plus difficile la suppression des ressources et des points de terminaison obsolètes de la base de code lorsqu'ils ne sont plus nécessaires. Je sais que c'est probablement subjectif car il est peu probable qu'il y ait une réponse simple et correcte, mais je suis curieux de comprendre comment les organisations qui gèrent des API complexes sur plusieurs versions résolvent ce problème.


41
Merci d'avoir posé cette question! JE NE PEUX PAS croire que plus de gens ne répondent pas à cette question !! J'en ai marre que tout le monde ait une opinion sur la façon dont les versions entrent dans un système, mais personne ne semble s'attaquer au vrai problème difficile de l'envoi des versions à leur code approprié. A présent, il devrait y avoir au moins un éventail de «modèles» ou de «solutions» acceptés à ce problème apparemment courant. Theres un nombre insensé de questions sur SO concernant "le contrôle de version d'API". Décider comment accepter les versions est FRIKKIN SIMPLE (relativement)! Le manipuler dans la base de code une fois qu'il entre, est DIFFICILE!
arijeet

Réponses:


45

J'ai utilisé les deux stratégies que vous mentionnez. De ces deux, je suis favorable à la deuxième approche, plus simple, dans les cas d'utilisation qui la supportent. Autrement dit, si les besoins de gestion des versions sont simples, optez pour une conception de logiciel plus simple:

  • Un faible nombre de changements, des changements de faible complexité ou un calendrier de changement à basse fréquence
  • Changements largement orthogonaux au reste de la base de code: l'API publique peut exister en paix avec le reste de la pile sans nécessiter de branchement "excessif" (quelle que soit la définition de ce terme que vous choisissez d'adopter) de branchement dans le code

Je n'ai pas trouvé trop difficile de supprimer les versions obsolètes à l'aide de ce modèle:

  • Une bonne couverture de test signifiait que l'extraction d'une API retirée et du code de support associé garantissait l'absence de régressions (enfin minimes)
  • Une bonne stratégie de dénomination (noms de packages versionnés par API ou versions d'API un peu plus laides dans les noms de méthodes) a facilité la localisation du code pertinent
  • Les préoccupations transversales sont plus difficiles; les modifications apportées aux systèmes principaux de base pour prendre en charge plusieurs API doivent être soigneusement pesées. À un moment donné, le coût de la gestion des versions du backend (voir le commentaire sur «excessif» ci-dessus) l'emporte sur les avantages d'une seule base de code.

La première approche est certainement plus simple du point de vue de la réduction des conflits entre les versions coexistantes, mais les frais généraux liés à la maintenance de systèmes séparés avaient tendance à l'emporter sur l'avantage de réduire les conflits de versions. Cela dit, il était extrêmement simple de créer une nouvelle pile d'API publique et de commencer à itérer sur une branche d'API distincte. Bien sûr, la perte générationnelle s'est installée presque immédiatement et les branches se sont transformées en un gâchis de fusions, de résolutions de conflits de fusion et d'autres choses amusantes.

Une troisième approche se situe au niveau de la couche architecturale: adoptez une variante du modèle Facade et abstenez vos API dans des couches versionnées destinées au public qui communiquent avec l'instance Facade appropriée, qui à son tour communique avec le backend via son propre ensemble d'API. Votre façade (j'ai utilisé un adaptateur dans mon projet précédent) devient son propre package, autonome et testable, et vous permet de migrer les API frontend indépendamment du backend et les uns des autres.

Cela fonctionnera si vos versions d'API ont tendance à exposer les mêmes types de ressources, mais avec des représentations structurelles différentes, comme dans votre exemple nom complet / prénom / nom. Cela devient un peu plus difficile s'ils commencent à s'appuyer sur différents calculs backend, comme dans "Mon service backend a renvoyé un intérêt composé incorrectement calculé qui a été exposé dans l'API publique v1. Nos clients ont déjà corrigé ce comportement incorrect. Par conséquent, je ne peux pas mettre à jour cela calcul dans le backend et faites-le appliquer jusqu'à la v2. Par conséquent, nous devons maintenant bifurquer notre code de calcul des intérêts. " Heureusement, ceux-ci ont tendance à être peu fréquents: en pratique, les consommateurs d'API RESTful préfèrent les représentations de ressources précises à la rétrocompatibilité bogue pour bogue, même parmi les changements non-rupture sur une GETressource théoriquement idempotente .

Je serai intéressé d'entendre votre décision éventuelle.


5
Juste curieux, dans le code source, dupliquez-vous des modèles entre v0 et v1 qui n'ont pas changé? Ou avez-vous v1 utiliser certains modèles v0? Pour moi, je serais confus si je voyais v1 utiliser des modèles v0 pour certains champs. Mais d'un autre côté, cela réduirait le gonflement du code. Pour gérer plusieurs versions, devons-nous simplement accepter et vivre avec du code duplicatif pour des modèles qui n'ont jamais changé?
EdgeCaseBerg

1
Je me souviens que nos modèles versionnés de code source indépendamment de l'API elle-même, par exemple, l'API v1 pourrait utiliser le modèle V1, et l'API v2 pourrait également utiliser le modèle V1. Fondamentalement, le graphique de dépendance interne pour l'API publique comprenait à la fois le code d'API exposé, ainsi que le code de «traitement» du backend tel que le code du serveur et du modèle. Pour plusieurs versions, la seule stratégie que j'ai jamais utilisée est la duplication de la pile entière - une approche hybride (le module A est dupliqué, le module B est versionné ...) semble très déroutant. YMMV bien sûr. :)
Palpatim

2
Je ne suis pas sûr de suivre ce qui est suggéré pour la troisième approche. Y a-t-il des exemples publics de code structuré comme ça?
Ehtesh Choudhury

13

Pour moi, la deuxième approche est meilleure. Je l'ai utilisé pour les services Web SOAP et je prévois de l'utiliser également pour REST.

Au fur et à mesure que vous écrivez, la base de code doit tenir compte de la version, mais une couche de compatibilité peut être utilisée comme couche distincte. Dans votre exemple, la base de code peut produire une représentation de ressource (JSON ou XML) avec le prénom et le nom, mais la couche de compatibilité la modifiera pour qu'elle n'ait qu'un nom à la place.

La base de code ne devrait implémenter que la dernière version, disons la v3. La couche de compatibilité doit convertir les demandes et les réponses entre la dernière version v3 et les versions prises en charge, par exemple v1 et v2. La couche de compatibilité peut avoir des adaptateurs distincts pour chaque version prise en charge qui peuvent être connectés en tant que chaîne.

Par exemple:

Demande du client v1: v1 s'adapter à v2 ---> v2 adapter à v3 ----> codebase

Demande du client v2: v1 s'adapter à la v2 (ignorer) ---> v2 s'adapter à la v3 ----> base de code

Pour la réponse, les adaptateurs fonctionnent simplement dans le sens opposé. Si vous utilisez Java EE, vous pouvez utiliser la chaîne de filtres de servlet comme chaîne d'adaptateur par exemple.

Supprimer une version est facile, supprimez l'adaptateur correspondant et le code de test.


Il est difficile de garantir la compatibilité si toute la base de code sous-jacente a changé. Il est beaucoup plus sûr de conserver l'ancienne base de code pour les versions de correction de bogues.
Marcelo Cantos

5

La ramification me semble beaucoup mieux et j'ai utilisé cette approche dans mon cas.

Oui, comme vous l'avez déjà mentionné - les corrections de bogues de rétroportage nécessiteront un certain effort, mais en même temps, la prise en charge de plusieurs versions sous une seule base source (avec le routage et tous les autres éléments) vous demandera sinon moins, mais au moins le même effort, rendant le système plus compliqué et monstrueux avec différentes branches de logique à l'intérieur (à un moment donné de la gestion des versions, vous arriverez certainement à case()pointer énormément vers les modules de version ayant du code dupliqué, ou ayant encore pire if(version == 2) then...). N'oubliez pas non plus qu'à des fins de régression, vous devez toujours garder les tests ramifiés.

En ce qui concerne la politique de gestion des versions: je conserverais au maximum -2 versions de la prise en charge actuelle et obsolète des anciennes, ce qui motiverait les utilisateurs à se déplacer.


Je pense à tester dans une seule base de code pour le moment. Vous avez mentionné que les tests devraient toujours être branchés, mais je pense que tous les tests pour v1, v2, v3, etc. pourraient également vivre dans la même solution et être tous exécutés en même temps. Je pense à la décoration des essais avec des attributs qui précisent les versions qu'ils prennent en charge: par exemple [Version(From="v1", To="v2")], [Version(From="v2", To="v3")], [Version(From="v1")] // All versions juste explorer maintenant, jamais entendu quelqu'un faire?
Lee Gunn

1
Eh bien, après 3 ans, j'ai appris qu'il n'y avait pas de réponse précise à la question initiale: D. Cela dépend beaucoup du projet. Si vous pouvez vous permettre de geler l'API et de la maintenir uniquement (par exemple, des corrections de bogues), je branche / détacherais toujours le code associé (logique métier liée à l'API + tests + point de terminaison de repos) et partagerais tous les éléments dans une bibliothèque séparée (avec ses propres tests ). Si V1 doit coexister avec V2 pendant un certain temps et que le travail sur les fonctionnalités est toujours en cours, je les garderais ensemble et les tests également (couvrant V1, V2, etc. et nommés en conséquence).
edmarisov

1
Merci. Ouais, cela semble être un espace assez opiniâtre. Je vais d'abord essayer l'approche à solution unique et voir comment cela se passe.
Lee Gunn

0

Habituellement, l'introduction d'une version majeure de l'API vous conduisant dans une situation de devoir maintenir plusieurs versions est un événement qui ne se produit pas (ou ne devrait pas) se produire très fréquemment. Cependant, cela ne peut pas être complètement évité. Je pense que c'est globalement une hypothèse sûre qu'une version majeure, une fois introduite, resterait la dernière version pendant une période de temps relativement longue. Sur cette base, je préférerais obtenir la simplicité du code au détriment de la duplication, car cela me donne une meilleure confiance pour ne pas casser la version précédente lorsque j'introduis des modifications dans la dernière.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.