Comment manger un éléphant [monolithique]?

Diriger l'évolution d'un logiciel «ancien» est un problème difficile à résoudre. La taille de la base de code, le degré élevé de couplage entre composants et la dette technique accumulée au fil des ans peuvent être accablants. Souvent, le manque de tests automatisés rend chaque changement technique risqué et potentiellement perturbateur pour l’entreprise. Pensez aux clients qui ne peuvent pas passer de commande en raison d’une mise à jour de la bibliothèque.

Comment les équipes abordent-elles cette situation inconfortable? Quand ils sont chargés d’un important exercice de refactorisation ou de migration, comment conçoivent-ils un plan de transition sûr? Comment commencent-ils et mesurent-ils les progrès?

Dans cet article, nous présentons une étude de cas pour montrer qu'un processus basé sur les données, basé sur la méthode objectif-question-métrique, peut apporter clarté et confiance à l'équipe. Après avoir introduit l'approche, nous décrivons certains des outils utilisés pour collecter les données tout au long du processus (avant et pendant la migration).

Étude de cas

Nous avons récemment aidé une équipe qui développe un produit B2B avec une architecture typique à plusieurs niveaux. Plusieurs applications Web interagissent avec une API REST, connectée à des services d’entreprise et d’accès aux données. Le logiciel a environ 7 ans. Bien que nous ayons travaillé sur des systèmes beaucoup plus grands, anciens et complexes, cette application a déjà une taille décente, avec des dizaines de points de terminaison.

Après un examen initial du code, nous avons identifié les problèmes communs aux systèmes hérités: dépendances non gérées et obsolètes, structure de paquet et de fichier érodée, manque de tests automatisés, manque d’automatisation de la construction.

Nous avons été invités à proposer une stratégie de migration technique et à travailler avec l'équipe pour la mettre en œuvre. Les principaux facteurs opérationnels de la migration étaient le coût de la maintenance et les risques de sécurité associés aux bibliothèques et aux frameworks obsolètes.

Notre recommandation était de ne pas scinder une application monolithique en une fédération de micro-services. Les micro-services distribués auraient ajouté de la complexité sans aucun avantage réel. Au lieu de cela, nous avons conseillé de transformer le monolithe en un monolithe plus gérable et plus robuste. Pour mettre en œuvre cette stratégie, nous avons conçu un processus incrémentiel:

  • Créez le squelette du nouveau monolithe.
  • Identifiez les contextes liés et extrayez-les un par un.
  • Au cours de la migration, les utilisateurs interagissent avec des services existants et «nouveaux».
  • Après la migration, les points de terminaison déconseillés et le code mort ont été éliminés.
  • Pour mettre en œuvre ce processus en toute sécurité, écrivez des tests automatisés (en appliquant les techniques BDD) pour décrire le comportement actuel de chaque contexte lié. Ces tests nous permettent de valider que le comportement des anciens et des nouveaux systèmes est le même.

Dans le diagramme ci-dessus, nous voyons:

  • Le monolithe hérité (1). Certains points de terminaison et du code n’étaient plus utilisés (mais nous ne savions pas combien).
  • Différents clients (2) qui interagissent avec le système backend.
  • Le nouveau monolithe (3), construit sur une structure propre, avec des dépendances gérées. Initialement, ce nouveau backend ne contient aucun noeud final. Au fil du temps, les ordinateurs d'extrémité et les services de support sont migrés sur cette nouvelle base de code.
  • API Gateway (4), que nous avons introduite pour activer le processus de migration incrémentielle. La passerelle achemine les requêtes HTTP vers le monolithe approprié. Au début de la migration, toutes les demandes sont routées vers le système hérité. A la fin, toutes les demandes sont envoyées au nouveau système.
  • Le faisceau de test BDD (5), qui est une suite de tests automatisés décrivant le comportement souhaité de l’application dorsale. Les tests sont écrits au début du processus pour décrire le comportement du monolithe hérité. Lors de la migration, lorsque les terminaux et le trafic sont déplacés sur le nouveau système, le faisceau de test sert à vérifier que le comportement du système n'a pas été modifié.
  • Un ensemble de tests (6) de bout en bout sont également créés pour valider le système via la couche d'interface utilisateur. Alors que les tests au niveau de la couche API doivent être complets, un nombre moins important de tests de bout en bout sont mis en œuvre (en se concentrant généralement sur la «voie heureuse»).

Comment commence-t-on?

Décrire le processus de migration laisse plusieurs questions en suspens. De combien de temps l'équipe aura-t-elle besoin pour le mettre en œuvre? Et au cours du processus, comment l’équipe sera-t-elle en mesure de mesurer les progrès et de revoir le délai de réalisation prévu?

Répondre à ces questions est très important pour donner de la visibilité à la direction et de la clarté à l'équipe. Sans cela, n'importe qui se sentirait mal à l'aise pour commencer l'exercice. C’est quelque chose que nous avons observé à plusieurs reprises: lorsque les équipes ne connaissent pas la taille de l’éléphant, il leur est très difficile de commencer à mordre.

Encadrer le problème avec Goal-Question-Metric

Pour aider l'équipe, nous avons appliqué la méthode Goal-Question-Metric. Dans ce cas particulier, nous avions deux objectifs:

  • Lors de l'évaluation de la situation avant la migration, notre objectif était d'estimer l'effort global. Nous avons posé 5 questions et pour chaque question, nous avons identifié une liste de métriques.
Goal Estimer l'effort de refactoring majeur
  Question Quelle est la taille du produit?
    Nombre de scénarios de bout en bout
    Nombre de composants de l'interface utilisateur (applications, pages, composants)
    Nombre métrique de points de terminaison REST
    Nombre métrique d'entités persistantes
    Nombre métrique de requêtes de base de données
    Nombre métrique de fichiers source
    Métrique Nombre de commits dans l'historique git
    Répartition par âge métrique des fichiers source
  Question Quelles sont les parties les plus importantes de l'application?
    Classement métrique des noeuds finaux REST par utilisation (à partir des journaux)
    Métrique Incidence commerciale estimée des scénarios
  Question Quelle est l’importance de la refactorisation?
    Métrique Nombre de bibliothèques / cadres à remplacer
    Métrique Nombre de bibliothèques à mettre à niveau
    Delta métrique entre les versions actuelle et cible des bibliothèques
    Métrique Nombre de fichiers source à modifier
  Question Comment pouvons-nous "en toute sécurité" refactoriser?
    Métrique% de scénarios de bout en bout avec des tests automatisés
    % Métrique de points de terminaison REST avec tests automatisés
    Couverture du code métrique
    Temps métrique requis pour tester manuellement l'application
    Sentiment métrique développeur
  Question Quelle quantité de code est encore utilisée?
    Métrique% de points de terminaison REST toujours utilisés dans les scénarios d'utilisation
    Métrique% de code couvert lors de la réalisation de scénarios
    Estimation du développeur métrique
  • Pendant la migration, notre objectif était de suivre les progrès et de mettre à jour les efforts restants. Nous avons posé 2 questions, encore une fois liées à des métriques spécifiques.
Objectif Suivre l'avancement de la refactorisation
  Question Dans quelle mesure avons-nous amélioré le filet de "sécurité"?
    Métrique% de scénarios de bout en bout avec tests automatisés + delta
    % Métrique de points de terminaison REST avec tests automatisés + delta
    Couverture du code métrique + delta
    Sentiment métrique développeur
  Question Combien de "services" avons-nous extraits et migrés?
    % Métrique de points de terminaison REST (avec agrégats) migrés
    Métrique Nombre de fichiers source supprimés de la version
    Temps métrique consacré à chaque automatisation de test
    Temps métrique passé à chaque extraction

Extraction de métriques

Avec la structure GQM en place, nous avons dû trouver un moyen de collecter les métriques, de manière automatisée. Nous avons des outils pour extraire les métriques de code, mais nous ne les discuterons pas dans cet article. Nous allons plutôt nous concentrer sur les 2 métriques de niveau supérieur suivantes, qui sont plus difficiles à collecter:

  • le pourcentage de points de terminaison REST toujours utilisés dans les scénarios d'utilisation
  • le pourcentage de code couvert lors de la réalisation de scénarios

Pour collecter les données, nous avons conçu un système d’enregistrement de scénarios d’utilisation. Le système génère des fichiers journaux qui relient chaque scénario à une liste de noeuds finaux et à une liste de méthodes exécutées. Par exemple, nous pouvons utiliser le système pour enregistrer le scénario «Créer une facture». Le système génère des métadonnées qui lient le scénario à 3 points de terminaison REST et à 12 méthodes.

Pour construire le système, nous avons intégré 3 composants principaux:

  • Nous avons utilisé une extension Chrome open source conçue pour faciliter les tests exploratoires. Il fournit un moyen facile d’enregistrer le début et la fin d’un scénario. Le responsable de produit peut parcourir l'intégralité de l'application et signaler le début et la fin de chaque scénario. Il peut leur donner des noms descriptifs. À la fin de la session d'enregistrement, l'extension Chrome nous fournit un premier journal des événements, avec la démarcation temporelle entre les scénarios.
  • Nous avons utilisé une simple passerelle API devant le monolith pour capturer les requêtes HTTP envoyées à l'API REST. Ce proxy nous fournit un deuxième journal des événements, dans lequel nous avons un horodatage pour chaque appel de noeud final.
  • Enfin, OpenClover est utilisé pour instrumenter le code de l'application et générer des métriques de couverture de code. Cela produit une troisième sortie de métadonnées horodatées.

Voici une vue simplifiée des trois fichiers journaux générés (OpenClover stocke les données dans une base de données et le processus d’enregistrement des sessions est un peu plus complexe):

* Journal des événements 1 (extension chrome)
12:02:00 début - facture client
12:03:12 fin - facture client
12:04:10 début - impression du rapport mensuel
12h05:00 fin - impression du rapport mensuel
* Journal des événements 2 (passerelle API)
12:02:04 POST / api / auth
12:02:30 GET / api / clients / 93
12:02:49 POST / api / tasks / sendInvoice
* Journal des événements 3 (OpenClover)
12:02:04 méthode com.acme.controllers.AuthController.login
12:02:04 méthode com.acme.services.AuthService.authenticate
...

Il est alors assez facile de traiter ces fichiers. Le premier est utilisé pour identifier les limites temporelles entre les scénarios. Les autres sont ensuite utilisés pour extraire les points de terminaison et les appels de méthodes qui se produisent dans ces limites. Une façon de rendre ces données faciles à utiliser consiste à générer un fichier CSV, puis à utiliser un outil de visualisation des données tel que Tableau.

Conclusion

Bien entendu, la configuration d’un tel système et l’enregistrement de chaque scénario d’utilisation (création d’un inventaire des fonctionnalités) prend un certain temps.

Mais lorsque cela est fait, l’équipe a quelque chose de concret et de quantifiable sur lequel travailler. L'équipe dispose d'un moyen concret et quantitatif pour suivre les progrès de la migration. De plus, les développeurs ont rapidement une idée de la quantité de points de terminaison déconseillés et de code mort.

Comme nous l’avons dit plus tôt, c’est souvent ce dont les équipes ont besoin pour commencer à travailler sur une tâche difficile et accablante. Qu'il s'agisse d'une refactorisation complexe, d'une initiative de remboursement de la dette technique ou d'une campagne d'introduction de pratiques de tests automatisés, la même approche de haut niveau peut être appliquée.