web-dev-qa-db-fra.com

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

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

Imaginons que nous apportions de nombreuses modifications à une API - par exemple, en modifiant notre ressource Client afin qu'elle renvoie des champs forename et surname séparés au lieu d'un seul name champ. (Pour cet exemple, j'utiliserai la solution de version 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 bugs 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 rédigeons-nous, testons-nous et déployons-nous les modifications sur notre serveur API? Je peux voir au moins deux solutions:

  • Utilisez une branche/balise de contrôle de source pour la base de code v1. v1 et v2 sont développées et déployées indépendamment, avec des fusions de contrôle de révision utilisées si nécessaire pour appliquer le même bugfix aux deux versions - similaire à la façon dont vous géreriez 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 d'API, de sorte que vous vous retrouvez avec une base de code unique qui inclut à 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 au lieu d'un problème de déploiement - en utilisant probablement une combinaison d'espaces de noms et de routage pour vous assurer que les demandes sont gérées par la bonne version.

L'avantage évident du modèle de branche est qu'il est trivial 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 "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 cela est probablement subjectif car il est peu probable qu'il y ait une réponse correcte simple, mais je suis curieux de comprendre comment les organisations qui maintiennent des API complexes sur plusieurs versions résolvent ce problème.

83
Dylan Beattie

J'ai utilisé les deux stratégies que vous mentionnez. De ces deux, je préfère la deuxième approche, plus simple, dans les cas d'utilisation qui la supportent. Autrement dit, si les besoins de version sont simples, optez pour une conception logicielle plus simple:

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

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

  • Une bonne couverture des tests signifiait que l'extraction d'une API retirée et du code de support associé n'assurait aucune régression (enfin, minimale)
  • Une bonne stratégie de dénomination (noms de package version API, ou quelque peu plus laid, versions API dans les noms de méthode) a facilité la localisation du code pertinent
  • Les préoccupations transversales sont plus difficiles; les modifications apportées aux principaux systèmes dorsaux pour prendre en charge plusieurs API doivent être soigneusement pesées. À un certain point, le coût du backend de version (voir le commentaire sur "excessif" ci-dessus) l'emporte sur l'avantage d'une base de code unique.

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 ont tendance à l'emporter sur l'avantage de la réduction des 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 fusion de résolutions de conflits et d'autres amusements similaires.

Une troisième approche se situe au niveau de la couche architecturale: adoptez une variante du modèle de façade, et résumez vos API dans des couches versionnées publiques qui parlent à l'instance de façade appropriée, qui à son tour parle au 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 frontales indépendamment du backend et les unes 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 de nom complet/prénom/nom de famille. Cela devient un peu plus difficile s'ils commencent à s'appuyer sur différents calculs de backend, comme dans "Mon service de backend a renvoyé des intérêts composés calculés incorrectement qui ont été exposés 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 le faire 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: pratiquement, les consommateurs d'API RESTful préfèrent les représentations de ressources précises à la compatibilité descendante bogue pour bogue, même parmi les changements incessants sur une ressource théoriquement idempotente GETted.

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

38
Palpatim

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 également.

Au moment où vous écrivez, la base de code doit être compatible avec 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 des ressources (JSON ou XML) avec le prénom et le nom, mais la couche de compatibilité le changera pour avoir uniquement un nom à la place.

La base de code ne devrait implémenter que la dernière version, disons 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:

Client v1 request: v1 adapt to v2 ---> v2 adapt to v3 ----> codebase

Client v2 request: v1 adapt to v2 (skip) ---> v2 adapt to v3 ----> codebase

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

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

13
S.Stavreva

Le branchement 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 des efforts, mais en même temps, la prise en charge de plusieurs versions sous une seule source (avec routage et tout le reste) vous demandera sinon moins, mais au moins le même effort, rendant le système plus compliqué et monstrueux avec différentes branches de la logique à l'intérieur (à un certain moment de la version, vous arriverez certainement à une énorme case() pointant vers des 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 branchés.

En ce qui concerne la politique de gestion des versions: je conserverais au maximum -2 versions de la prise en charge obsolète des anciennes - ce qui donnerait une motivation aux utilisateurs pour se déplacer.

5
edmarisov