Comment mon équipe peut-elle éviter les erreurs fréquentes après refactoring?


20

Pour vous donner un petit aperçu: je travaille pour une entreprise avec environ douze développeurs Ruby on Rails (+/- stagiaires). Le travail à distance est courant. Notre produit est composé de deux parties: un noyau plutôt gras, et mince pour les grands projets clients construits sur lui. Les projets clients élargissent généralement le cœur. L'écrasement des fonctionnalités clés ne se produit pas. Je pourrais ajouter que le noyau a quelques parties plutôt mauvaises qui ont un besoin urgent de refactorisations. Il y a des spécifications, mais surtout pour les projets clients. La pire partie du noyau n'est pas testée (pas comme il se doit ...).

Les développeurs sont divisés en deux équipes, travaillant avec un ou deux PO pour chaque sprint. Habituellement, un projet client est strictement associé à l'une des équipes et des bons de commande.

Maintenant notre problème: assez souvent, nous nous cassons les affaires. Un membre de l'équipe A étend ou remanie la fonctionnalité principale Y, provoquant des erreurs inattendues pour l'un des projets clients de l'équipe B. Généralement, les changements ne sont pas annoncés par les équipes, donc les bugs sont presque toujours inattendus. L'équipe B, y compris le PO, a pensé que la fonctionnalité Y était stable et ne l'a pas testée avant de la relâcher, ignorant les changements.

Comment se débarrasser de ces problèmes? Quel type de «technique d'annonce» pouvez-vous me recommander?


34
La réponse évidente est TDD .
mouviciel

1
Comment vous vient déclarez que « Écrasement des principales caractéristiques ne se produit pas », et votre problème est qu'il ne se produise? Faites-vous la différence dans votre équipe entre "noyau" et "fonctionnalités clés", et comment procédez-vous? J'essaie juste de comprendre la situation ...
logc

4
@mouvciel Cela et n'utilisez pas la frappe dynamique , mais ce conseil particulier arrive un peu trop tard dans ce cas.
Doval

3
Utilisez un langage fortement typé comme OCaml.
Gaius

@logc Peut-être que je n'étais pas clair, désolé. Nous ne remplaçons pas une fonctionnalité de base comme la bibliothèque de filtres elle-même, mais ajoutons de nouveaux filtres aux classes que nous utilisons dans nos projets clients. Un scénario courant peut être que les changements dans la bibliothèque de filtres détruisent les filtres ajoutés dans le projet client.
SDD64 du

Réponses:


24

Je recommanderais de lire Travailler efficacement avec le code hérité de Michael C. Feathers . Il explique que vous avez vraiment besoin de tests automatisés, comment vous pouvez facilement les ajouter, si vous ne les avez pas déjà, et quel "code sent" pour refactoriser de quelle manière.

En plus de cela, un autre problème central dans votre situation semble être un manque de communication entre les deux équipes. Quelle est la taille de ces équipes? Travaillent-ils sur différents arriérés?

C'est presque toujours une mauvaise pratique de diviser les équipes selon votre architecture. Par exemple, une équipe principale et une équipe non principale. Au lieu de cela, je créerais des équipes sur un domaine fonctionnel, mais multi-composants.


J'ai lu dans "The Mythical Man-Month" que la structure du code suit généralement la structure de l'équipe / organisation. Ainsi, ce n'est pas vraiment une "mauvaise pratique", mais juste la façon dont les choses se passent habituellement.
Marcel

Je pense que dans " Dynamics of software development ", le responsable de Visual C ++ recommande vivement d'avoir des équipes de fonctionnalités; Je n'ai pas lu "The Mythical Man-Month", @Marcel, mais AFAIK il répertorie les mauvaises pratiques dans l'industrie ...
logc

Marcel, c'est vrai que c'est comme ça que les choses se passent ou se passent habituellement, mais de plus en plus d'équipes le font différemment, par exemple les équipes de longs métrages. Le fait d'avoir des équipes basées sur des composants entraîne un manque de communication lorsque vous travaillez sur des fonctionnalités inter-composants. À côté de cela, il en résultera presque toujours des discussions architecturales non basées sur le but d'une bonne architecture, mais sur des personnes essayant de pousser les responsabilités vers d'autres équipes / composants. Par conséquent, vous obtiendrez la situation décrite par l'auteur de cette question. Voir aussi mountaingoatsoftware.com/blog/the-benefits-of-feature-teams .
Tohnmeister

Eh bien, pour autant que je comprenne le PO, il a déclaré que les équipes ne sont pas divisées en une équipe principale et une équipe secondaire. Les équipes sont réparties "par client", c'est-à-dire essentiellement "par domaine fonctionnel". Et cela fait partie du problème: puisque toutes les équipes sont autorisées à changer le tronc commun, les changements d'une équipe affectent l'autre.
Doc Brown

@DocBrown Vous avez raison. Chaque équipe peut changer le noyau. Bien entendu, ces changements sont censés être bénéfiques pour chaque projet. Cependant, ils travaillent sur différents arriérés. Nous en avons un pour chaque client et un pour le cœur.
SDD64

41

La pire partie du noyau n'est pas testée (comme il se doit ...).

C'est le problème. Une refactorisation efficace dépend fortement de la suite de tests automatisés. Si vous n'en avez pas, les problèmes que vous décrivez commencent à apparaître. Ceci est particulièrement important si vous utilisez un langage dynamique comme Ruby, où il n'y a pas de compilateur pour intercepter les erreurs de base liées au passage des paramètres aux méthodes.


10
Cela et refactoring par étapes et s'engager très fréquemment.
Stefan Billiet

1
Il y a probablement des tas de conseils qui pourraient ajouter des conseils ici, mais tout se résumera à ce point. Quelle que soit la blague "comme il se doit" montrant qu'ils savent que c'est un problème en soi, l'impact des tests scriptés sur le refactoring est immense: si une passe est devenue un échec, alors le refactoring n'a pas fonctionné. Si toutes les passes restent des passes, le refactoring pourrait avoir fonctionné (le déplacement échoue aux passes serait évidemment un plus, mais garder toutes les passes comme des passes est plus important que même un gain net; un changement qui casse un test et en corrige cinq pourrait être un amélioration, mais pas une refactorisation)
Jon Hanna

Je vous ai donné un "+1", mais je pense que les "tests automatisés" ne sont pas la seule approche pour résoudre ce problème. Un meilleur AQ manuel, mais systématique, peut-être par une équipe AQ ​​distincte, pourrait également résoudre les problèmes de qualité (et il est probablement logique d'avoir à la fois - des tests automatiques et manuels).
Doc Brown

Un bon point, mais si le noyau et les projets clients sont des modules séparés (et en plus dans un langage dynamique comme Ruby), le noyau peut changer à la fois un test et son implémentation associée , et casser un module dépendant sans échouer ses propres tests.
logc

Comme d'autres l'ont fait remarquer. TDD. Vous reconnaissez probablement déjà que vous devriez avoir des tests unitaires pour autant de code que possible. Bien que l'écriture de tests unitaires juste pour le plaisir soit une perte de ressources, lorsque vous commencez à refactoriser un composant, vous devez commencer par une écriture de test approfondie avant de toucher au code principal.
jb510

5

Les réponses précédentes qui vous indiquent de meilleurs tests unitaires sont bonnes, mais je pense qu'il pourrait y avoir des problèmes plus fondamentaux à résoudre. Vous avez besoin d'interfaces claires pour accéder au code principal à partir du code des projets clients. De cette façon, si vous refactorisez le code principal sans altérer le comportement observé par le biais des interfaces , le code de l'autre équipe ne se cassera pas. Il sera ainsi beaucoup plus facile de savoir ce qui peut être refactorisé "en toute sécurité" et ce qui nécessite une refonte, éventuellement une rupture d'interface.


Repérez. Des tests plus automatisés n'apporteront que des avantages et en valent la peine, mais cela ne résoudra pas le problème principal ici, qui est un échec de communication des changements de base. Le découplage en enveloppant les interfaces autour des fonctionnalités importantes sera une énorme amélioration.
Bob Tway

5

D'autres réponses ont mis en évidence des points importants (davantage de tests unitaires, des équipes de fonctionnalités, des interfaces propres aux composants principaux), mais il y a un point que je trouve manquant, qui est le versioning.

Si vous gelez le comportement de votre core en effectuant une version 1 et que vous placez cette version dans un système de gestion d'artefact privé 2 , tout projet client peut déclarer sa dépendance à l'égard de la version de base X , et il ne sera pas interrompu par la prochaine version X + 1 .

La "politique d'annonce" se réduit alors à avoir un fichier CHANGES avec chaque version, ou à avoir une réunion d'équipe pour annoncer toutes les fonctionnalités de chaque nouvelle version principale.

De plus, je pense que vous devez mieux définir ce qui est "noyau" et quel sous-ensemble est "clé". Vous semblez (correctement) éviter d'apporter de nombreuses modifications aux "composants clés", mais vous autorisez des modifications fréquentes du "noyau". Pour pouvoir compter sur quelque chose, vous devez le garder stable; si quelque chose n'est pas stable, ne l'appelez pas core. Peut-être pourrais-je suggérer de l'appeler "composants auxiliaires"?

EDIT : Si vous suivez les conventions du système de version sémantique , tout changement incompatible dans l'API du noyau doit être marqué par un changement de version majeur . Autrement dit, lorsque vous modifiez le comportement du noyau existant précédemment, ou supprimez quelque chose, et pas seulement ajoutez quelque chose de nouveau. Avec cette convention, les développeurs savent que la mise à jour de la version «1.1» vers «1.2» est sûre, mais passer de «1.X» à «2.0» est risqué et doit être soigneusement examiné.

1: Je pense que cela s'appelle une gemme, dans le monde de Ruby
2: l'équivalent de Nexus en Java ou PyPI en Python


Le "versioning" est important, en effet, mais quand on essaie de résoudre le problème décrit en gelant le noyau avant une version, alors on se retrouve facilement avec le besoin de branchements et de fusions sophistiqués. Le raisonnement est que pendant une phase de "build build" de l'équipe A, A devra peut-être changer le noyau (au moins pour la correction de bogues), mais n'acceptera pas les modifications du noyau d'autres équipes - donc vous vous retrouvez avec une branche de le noyau par équipe, à fusionner "plus tard", qui est une forme de dette technique. C'est parfois correct, mais souvent cela ne fait que reporter le problème décrit à un moment ultérieur.
Doc Brown

@DocBrown: Je suis d'accord avec vous, mais j'ai écrit en supposant que tous les développeurs sont coopératifs et adultes. Cela ne veut pas dire que je n'ai pas vu ce que vous décrivez . Mais un élément clé pour rendre un système fiable est, eh bien, la recherche de stabilité. De plus, si l' équipe A doit changer X dans le noyau, et les besoins équipe B au changement X dans le noyau, alors peut - être X ne pas appartiennent dans le noyau; Je pense que c'est mon autre point. :)
logc

@DocBrown Oui, nous avons appris à utiliser une branche du cœur pour chaque projet client. Cela a causé d'autres problèmes. Par exemple, nous n'aimons pas «toucher» les systèmes clients déjà déployés. En conséquence, ils peuvent rencontrer plusieurs sauts de version mineurs de leur noyau utilisé après chaque déploiement.
SDD64

@ SDD64: c'est exactement ce que je dis - ne pas intégrer immédiatement les modifications à un noyau commun n'est pas non plus une solution à long terme. Ce dont vous avez besoin, c'est d'une meilleure stratégie de test pour votre cœur - avec des tests automatiques et manuels également.
Doc Brown

1
Pour mémoire, je ne préconise pas un noyau séparé pour chaque équipe, ni ne nie que des tests sont requis - mais un test de base et sa mise en œuvre peuvent changer en même temps, comme je l'ai déjà dit . Seul un noyau figé, marqué par une chaîne de version ou une balise de validation, peut être utilisé par un projet qui s'appuie dessus (à l'exception des corrections de bogues et à condition que la stratégie de version soit saine).
logc

3

Comme d'autres l'ont dit, une bonne suite de tests unitaires ne résoudra pas votre problème: vous rencontrerez des problèmes lors de la fusion des modifications, même si chaque suite de tests d'équipe réussit.

Idem pour TDD. Je ne vois pas comment cela peut résoudre ce problème.

Votre solution n'est pas technique. Vous devez définir clairement les limites «fondamentales» et attribuer un rôle de «chien de garde» à quelqu'un, que ce soit le développeur principal ou l'architecte. Toute modification du noyau doit passer par ce chien de garde. Il est responsable de s'assurer que chaque sortie de toutes les équipes fusionnera sans trop de dommages collatéraux.


Nous avions un "chien de garde", car il a écrit la majeure partie du noyau. Malheureusement, il était également responsable de la plupart des parties non testées. Il s'est fait passer pour YAGNI et a été remplacé il y a six mois par deux autres gars. Nous avons encore du mal à refactoriser ces «parties sombres».
SDD64

2
L'idée est d'avoir une suite de tests unitaires pour le noyau , qui fait partie du noyau , avec des contributions de toutes les équipes, et non des suites de tests distinctes pour chaque équipe.
Doc Brown

2
@ SDD64: vous semblez confondre "Vous n'en aurez pas besoin (encore)" (ce qui est une très bonne chose) avec "Vous n'avez pas besoin de nettoyer votre code (encore)" - ce qui est une très mauvaise habitude , et à mon humble avis tout à fait le contraire.
Doc Brown

La solution de surveillance est vraiment, vraiment sous-optimale, à mon humble avis. C'est comme construire un seul point de défaillance dans votre système, et en plus un très lent, car cela implique une personne et de la politique. Sinon, TDD peut bien sûr aider à résoudre ce problème: chaque test de base est un exemple pour les développeurs de projet client comment le noyau actuel est censé être utilisé. Mais je pense que vous avez donné votre réponse de bonne foi ...
logc

@DocBrown: D'accord, peut-être que nos compréhensions diffèrent. Les fonctionnalités de base, écrites par lui, sont trop compliquées pour satisfaire même les possibilités les plus étranges. La plupart d'entre eux, nous n'en avons jamais rencontrés. La complexité nous ralentit pour les refactoriser, de l'autre côté.
SDD64

2

En tant que solution à plus long terme, vous avez également besoin d'une communication meilleure et plus rapide entre les équipes. Chacune des équipes qui utiliseront jamais, par exemple, la fonctionnalité de base Y, doit être impliquée dans la construction des scénarios de test prévus pour la fonctionnalité. Cette planification, en elle-même, mettra en évidence les différents cas d'utilisation inhérents à la fonctionnalité Y entre les deux équipes. Une fois que la fonctionnalité devrait fonctionner, et que les cas de test sont implémentés et convenus, un changement supplémentaire dans votre schéma d'implémentation est requis. L'équipe qui publie la fonctionnalité est nécessaire pour exécuter le testcase, pas l'équipe qui est sur le point de l'utiliser. La tâche, le cas échéant, qui devrait provoquer des collisions, est l'ajout d'un nouveau testcase de l'une des équipes. Lorsqu'un membre de l'équipe pense à un nouvel aspect de la fonctionnalité qui n'est pas testé, ils devraient être libres d'ajouter un testcase qu'ils ont vérifié en passant dans leur propre bac à sable. De cette façon, les seules collisions qui se produiront seront au niveau de l'intention et devraient être clouées avant que la fonction refactorisée ne soit libérée dans la nature.


2

Bien que chaque système ait besoin de suites de tests efficaces (ce qui signifie, entre autres choses, l'automatisation), et bien que ces tests, s'ils sont utilisés efficacement, détectent ces conflits plus tôt qu'ils ne le sont actuellement, cela ne résout pas les problèmes sous-jacents.

La question révèle au moins deux problèmes sous-jacents: la pratique de modifier le «noyau» afin de satisfaire les exigences des clients individuels, et l'incapacité des équipes à communiquer et à coordonner leur intention d'apporter des changements. Aucune de ces causes n'est à l'origine et vous devrez comprendre pourquoi cela est fait avant de pouvoir y remédier.

L'une des premières choses à déterminer est de savoir si les développeurs et les gestionnaires réalisent qu'il y a un problème ici. Si au moins certains le font, alors vous devez savoir pourquoi ils pensent qu'ils ne peuvent rien faire ou choisissent de ne pas le faire. Pour ceux qui ne le font pas, vous pouvez essayer d'augmenter leur capacité à anticiper comment leurs actions actuelles peuvent créer des problèmes futurs, ou les remplacer par des personnes qui le peuvent. Tant que vous n'aurez pas une main-d'œuvre consciente de la façon dont les choses vont mal, il est peu probable que vous puissiez résoudre le problème (et peut-être même pas à ce moment-là, du moins à court terme).

Il peut être difficile d'analyser le problème en termes abstraits, du moins au début, alors concentrez-vous sur un incident spécifique qui a entraîné un problème et essayez de déterminer comment il s'est produit. Comme les personnes impliquées sont susceptibles d'être sur la défensive, vous devrez être attentif aux justifications égoïstes et post-hoc afin de savoir ce qui se passe réellement.

Il y a une possibilité que j'hésite à mentionner car elle est si peu probable: les exigences des clients sont si disparates qu'il n'y a pas suffisamment de points communs pour justifier un code de base partagé. Si tel est le cas, vous disposez en fait de plusieurs produits distincts et vous devez les gérer en tant que tels et ne pas créer de couplage artificiel entre eux.


Avant de migrer notre produit de Java vers RoR, nous avons en fait fait comme vous l'avez suggéré. Nous avions un noyau Java pour tous les clients, mais leurs exigences l'ont brisé un jour et nous avons dû le diviser. Au cours de cette situation, nous avons dû faire face à des problèmes tels que: «Mec, le client Y possède une fonctionnalité de base tellement intéressante. Dommage que nous ne puissions pas le porter sur le client Z, car leur cœur est incompatible ». Avec Rails, nous voulons strictement opter pour une politique «un noyau pour tous». Si tel est le cas, nous proposons toujours des changements drastiques, mais ceux-ci dissocient le client de toute autre mise à jour.
SDD64

Il ne me suffit pas d'appeler TDD. Donc, outre le fractionnement de la suggestion de base, j'aime le plus votre réponse. Malheureusement, le noyau n'est pas parfaitement testé, mais cela ne résoudrait pas tous nos problèmes. L'ajout de nouvelles fonctionnalités de base pour un client peut sembler parfaitement bien et même donner une construction verte, pour eux, car seules les spécifications de base sont partagées entre les clients. On ne remarque pas ce qui arrive à chaque client possible. Donc, j'aime votre suggestion pour découvrir les problèmes et parler de ce qui les a causés.
SDD64

1

Nous savons tous que les tests unitaires sont la voie à suivre. Mais nous savons également qu'il est difficile de les réadapter de manière réaliste à un noyau.

Une technique spécifique qui peut vous être utile lors de l'extension de la fonctionnalité consiste à essayer de vérifier temporairement et localement que la fonctionnalité existante n'a pas été modifiée. Cela peut être fait comme ceci:

Pseudo-code d'origine:

def someFunction
   do original stuff
   return result
end

Code de test temporaire sur place:

def someFunctionNew
   new do stuff
   return result
end

def someFunctionOld
   do original stuff
   return result
end

def someFunction
   oldResult = someFunctionOld
   newResult = someFunctionNew
   check oldResult = newResult
   return newResult
end

Exécutez cette version à travers les tests de niveau système existants. Si tout va bien, vous savez que vous n'avez pas cassé les choses et pouvez alors supprimer l'ancien code. Notez que lorsque vous vérifiez l'ancienne et la nouvelle correspondance des résultats, vous pouvez également ajouter du code pour analyser les différences afin de capturer les cas qui, selon vous, devraient être différents en raison d'une modification prévue, comme une correction de bogue.


1

"La plupart du temps, les changements ne sont pas annoncés par les équipes, donc les bugs sont presque toujours inattendus"

Problème de communication n'importe qui? Qu'en est-il (en plus de ce que tout le monde a déjà souligné, que vous devriez faire des tests rigoureux) pour vous assurer qu'il y a une bonne communication? Que les gens sont informés que l'interface vers laquelle ils écrivent va changer dans la prochaine version et quelles seront ces modifications?
Et donnez-leur accès à au moins une interface factice (avec une implémentation vide) dès que possible pendant le développement afin qu'ils puissent commencer à écrire leur propre code.

Sans tout cela, les tests unitaires ne feront pas grand-chose, sauf lors des étapes finales, il y a quelque chose qui ne va pas entre les différentes parties du système. Vous voulez le savoir, mais vous voulez le savoir tôt, très tôt, et faire en sorte que les équipes se parlent, coordonnent les efforts et aient en fait un accès fréquent au travail que fait l'autre équipe (donc des engagements réguliers, pas un énorme s'engager après plusieurs semaines ou mois, 1-2 jours avant la livraison).
Votre bogue n'est PAS dans le code, certainement pas dans le code de l'autre équipe qui ne savait pas que vous jouiez avec l'interface contre laquelle ils écrivent. Votre bug est dans votre processus de développement, le manque de communication et de collaboration entre les gens. Ce n'est pas parce que vous êtes assis dans des pièces différentes que vous devez vous isoler des autres gars.


1

Principalement, vous avez un problème de communication (probablement aussi lié à un team building problème de ), donc je pense qu'une solution à votre cas devrait être axée sur ... eh bien, la communication, plutôt que les techniques de développement.

Je tiens pour acquis qu'il n'est pas possible de geler ou de bifurquer le module de base lors du démarrage d'un projet client (sinon vous devez simplement intégrer dans les plannings de votre entreprise certains projets non liés au client qui visent à mettre à jour le module de base).

Il nous reste donc à essayer d'améliorer la communication entre les équipes. Cela peut être résolu de deux manières:

  • avec les êtres humains. Cela signifie que votre entreprise désigne quelqu'un comme l' architecte du module principal (ou tout jargon qui est bon pour la direction) qui sera responsable de la qualité et de la disponibilité du code. Cette personne incarnera le noyau. Ainsi, elle sera partagée par toutes les équipes et assurera une bonne synchronisation entre elles. En outre, elle devrait également agir en tant que réviseur du code engagé dans le module de base pour maintenir sa cohérence;
  • avec des outils et des workflows. En imposant l' intégration continue au cœur, vous ferez du code cœur lui-même le support de communication. Cela nécessitera d'abord des efforts (par l'ajout de suites de tests automatisés), mais ensuite les rapports de CI nocturnes constitueront une mise à jour de l'état brut du module principal.

Vous pouvez en savoir plus sur CI en tant que processus de communication ici .

Enfin, vous avez toujours un problème avec le manque de travail d'équipe au niveau de l'entreprise. Je ne suis pas un grand fan des événements de team building, mais cela semble être un cas où ils seraient utiles. Organisez-vous régulièrement des réunions à l’échelle des développeurs? Pouvez-vous inviter des personnes d'autres équipes à vos rétrospectives de projet? Ou peut-être prendre de la bière le vendredi soir parfois?

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.