Jusqu'où puis-je pousser le refactoring sans modifier le comportement externe?


27

Selon Martin Fowler , le refactoring de code est (c'est moi qui souligne):

Le refactoring est une technique disciplinée pour restructurer un corps de code existant, en modifiant sa structure interne sans changer son comportement externe . Son cœur est une série de petits comportements préservant les transformations. Chaque transformation (appelée «refactoring») ne fait pas grand-chose, mais une séquence de transformations peut produire une restructuration importante. Étant donné que chaque refactoring est petit, il est moins susceptible de mal tourner. Le système est également maintenu en parfait état de fonctionnement après chaque petite refactorisation, ce qui réduit les risques qu'un système puisse être sérieusement endommagé pendant la restructuration.

Qu'est-ce que le «comportement extérieur» dans ce contexte? Par exemple, si j'applique le refactoring de la méthode move et que je déplace une méthode vers une autre classe, il semble que je change de comportement externe, n'est-ce pas?

Donc, je veux savoir à quel moment un changement cesse d'être un refactor et devient quelque chose de plus. Le terme «refactoring» peut être utilisé à mauvais escient pour des changements plus importants: y a-t-il un mot différent pour cela?

Mise à jour. Beaucoup de réponses intéressantes sur l'interface, mais le fait de ne pas déplacer la refactorisation de méthode ne changerait-il pas l'interface?


Si le comportement existant est nul ou incomplet, modifiez-le, supprimez / réécrivez-le. Vous pourriez ne pas refactoriser alors, mais peu importe le nom si le système s'améliorera (n'est-ce pas?) À la suite de cela.
Job

2
Votre superviseur peut se soucier si vous avez été autorisé à refactoriser et que vous avez fait une réécriture.
JeffO

2
Les limites du refactoring sont les tests unitaires. Si vous avez une spécification établie par eux, tout ce que vous changez qui ne casse pas les tests est une refactorisation?
George Silva


Si vous souhaitez en savoir plus, ce sujet est également un domaine de recherche actif. Il existe de nombreuses publications scientifiques sur ce sujet, par exemple, informatik.uni-trier.de/~ley/db/indices/a-tree/s/…
Skarab

Réponses:


25

"Externe" dans ce contexte signifie "observable par les utilisateurs". Les utilisateurs peuvent être humains dans le cas d'une application, ou d'autres programmes dans le cas d'une API publique.

Donc, si vous déplacez la méthode M de la classe A à la classe B, et que les deux classes sont au cœur d'une application, et qu'aucun utilisateur ne peut observer de changement dans le comportement de l'application en raison du changement, vous pouvez à juste titre l'appeler refactoring.

Si, OTOH, un autre sous-système / composant de niveau supérieur change de comportement ou se casse en raison du changement, cela est en effet (généralement) observable par les utilisateurs (ou au moins par les administrateurs système qui vérifient les journaux). Ou si vos classes faisaient partie d'une API publique, il peut y avoir du code tiers qui dépend du fait que M fasse partie de la classe A, pas B. Donc, aucun de ces cas n'est refactoring au sens strict.

il y a une tendance à appeler toute refonte de code comme une refactorisation qui est, je suppose, incorrecte.

En effet, c'est une conséquence triste mais attendue du refactoring devenu à la mode. Les développeurs ont fait un remaniement de code de manière ad hoc depuis des lustres, et il est certainement plus facile d'apprendre un nouveau mot à la mode que d'analyser et de changer des habitudes enracinées.

Alors, quel est le bon mot pour des retouches qui changent le comportement externe?

J'appellerais cela une refonte .

Mise à jour

Beaucoup de réponses intéressantes sur l'interface, mais le fait de ne pas déplacer la refactorisation de méthode ne changerait-il pas l'interface?

De quoi? Les classes spécifiques, oui. Mais ces classes sont-elles directement visibles du monde extérieur d'une manière ou d'une autre? Sinon - car ils sont à l'intérieur de votre programme, et ne font pas partie de l'interface externe (API / GUI) du programme - aucune modification apportée n'est observable par des parties externes (à moins que la modification ne casse quelque chose, bien sûr).

Je pense qu'il y a une question plus profonde au-delà de cela: une classe spécifique existe-t-elle en tant qu'entité indépendante par elle-même? Dans la plupart des cas, la réponse est non : la classe n'existe que dans le cadre d'un composant plus large, un écosystème de classes et d'objets, sans lequel elle ne peut pas être instanciée et / ou est inutilisable. Cet écosystème n'inclut pas seulement ses dépendances (directes / indirectes), mais aussi d'autres classes / objets qui en dépendent. En effet, sans ces classes de niveau supérieur, la responsabilité associée à notre classe peut être vide de sens / inutile pour les utilisateurs du système.

Par exemple, dans notre projet qui traite de la location de voitures, il y a un Chargeclasse. Cette classe n'a aucune utilité pour les utilisateurs du système en elle-même, car les agents de location et les clients ne peuvent pas faire grand-chose avec une charge individuelle: ils traitent les contrats de location dans leur ensemble (qui incluent un tas de différents types de charges) . Les utilisateurs sont surtout intéressés par la somme totale de ces frais, qu'ils doivent payer au final; l'agent est intéressé par les différentes options du contrat, la durée de la location, le groupe de véhicules, le package d'assurance, les articles supplémentaires, etc. etc. sélectionnés, qui (via des règles commerciales sophistiquées) régissent les frais présents et la manière dont le paiement final est calculé hors de ceux-ci. Et les représentants des pays / analystes commerciaux se soucient des règles commerciales spécifiques, de leur synergie et de leurs effets (sur les revenus de l'entreprise, etc.).

Récemment, j'ai refactorisé cette classe, renommant la plupart de ses champs et méthodes (pour suivre la convention de nommage Java standard, qui était totalement négligée par nos prédécesseurs). Je prévois aussi refactoring plus pour remplacer Stringet charchamps avec plus appropriés enumet booleantypes. Tout cela va certainement changer l'interface de la classe, mais (si je fais correctement mon travail), rien ne sera visible aux utilisateurs de notre application. Aucun d'entre eux ne se soucie de la façon dont les charges individuelles sont représentées, même s'ils connaissent sûrement le concept de charge. J'aurais pu sélectionner comme exemple une centaine d'autres classes ne représentant aucun concept de domaine, donc étant même conceptuellement invisible pour les utilisateurs finaux, mais j'ai pensé qu'il était plus intéressant de choisir un exemple où il y a au moins une certaine visibilité au niveau du concept. Cela montre bien que les interfaces de classe ne sont que des représentations de concepts de domaine (au mieux), pas la vraie chose *. La représentation peut être modifiée sans affecter le concept. Et les utilisateurs ont et comprennent seulement le concept; c'est notre tâche de faire la correspondance entre concept et représentation.

* Et on peut facilement ajouter que le modèle de domaine, que notre classe représente, n'est lui-même qu'une représentation approximative de quelque "chose réelle" ...


3
taquiner, je dirais «changement de conception» pas refonte. la refonte semble trop importante.
user606723

4
intéressant - dans votre exemple, la classe A est «un corps de code existant», et si la méthode M est publique, le comportement externe de A est modifié. Vous pourriez donc probablement dire que la classe A est repensée, tandis que le système global est en cours de refactorisation.
saus

J'aime observable pour les utilisateurs. C'est pourquoi je ne dirais pas que la rupture des tests unitaires est un signe, mais plutôt que les tests de bout en bout ou d'intégration seraient un signe.
Andy Wiesendanger

8

Externe signifie simplement interface dans son vrai sens lingual. Considérez une vache pour cet exemple. Tant que vous nourrissez des légumes et obtenez du lait comme valeur de retour, vous ne vous souciez pas du fonctionnement de ses organes internes. Maintenant, si Dieu change les organes internes des vaches, de sorte que son sang devienne bleu, tant que le point d'entrée et le point de sortie (bouche et lait) ne changent pas, cela peut être considéré comme une refactorisation.


1
Je ne pense pas que ce soit une bonne réponse. Le refactoring peut impliquer des changements d'API. Mais cela ne devrait pas avoir d'incidence sur l'utilisateur final ni sur la façon dont les entrées / fichiers sont lus / générés.
rds

3

Pour moi, le refactoring a été plus productif / confortable lorsque les limites ont été fixées par des tests et / ou par des spécifications formelles.

  • Ces limites sont suffisamment rigides pour que je me sente en sécurité sachant que si je traverse de temps en temps, il sera détecté assez tôt pour que je n'aie pas à annuler beaucoup de changements pour récupérer. D'un autre côté, ceux-ci donnent une marge de manœuvre suffisante pour améliorer le code sans se soucier de changer les comportements non pertinents.

  • Ce que j'aime particulièrement, c'est que ces types de frontières sont pour ainsi dire adaptatifs . Je veux dire, 1) je fais le changement et vérifie qu'il est conforme aux spécifications / tests. Ensuite, 2) il est passé au QA ou aux tests utilisateurs - notez ici, il peut toujours échouer car quelque chose manque dans les spécifications / tests. OK, si 3a) les tests réussissent, j'ai terminé, très bien. Sinon, si 3b) les tests échouent, je 4) annule la modification et 5) ajoute des tests ou clarifie les spécifications afin que la prochaine fois, cette erreur ne se répète pas. Notez que peu importe si les tests réussissent ou échouent, je gagne quelque chose - que ce soit du code / des tests / des spécifications s'améliore - mes efforts ne se transforment pas en gaspillage total.

Quant aux autres types de frontières - jusqu'à présent, je n'ai pas eu beaucoup de chance.

"Observable pour les utilisateurs" est une valeur sûre si l' on a une discipline pour le suivre - ce qui pour moi a toujours impliqué beaucoup d'efforts dans l'analyse des tests existants / création de nouveaux tests - peut-être trop d'efforts. Une autre chose que je n'aime pas dans cette approche est que la suivre aveuglément peut s'avérer trop restrictive. - Ce changement est interdit car avec lui le chargement des données prendra 3 sec au lieu de 2. - Uhm bien que diriez-vous de vérifier avec les utilisateurs / UX expert si cela est pertinent ou non? - Pas question, tout changement de comportement observable est interdit, point final. Sûr? tu paries! productif? pas vraiment.

Un autre que j'ai essayé est de garder la logique du code (la façon dont je le comprends lors de la lecture). Sauf pour les changements les plus élémentaires (et généralement pas très fructueux), celui-ci a toujours été une boîte de vers ... ou devrais-je dire une boîte de bugs? Je veux dire des bugs de régression. Il est tout simplement trop facile de casser quelque chose d'important lorsque vous travaillez avec du code spaghetti.


3

Le livre Refactoring est assez fort dans son message que vous ne pouvez effectuer de refactoring que lorsque vous avez une couverture de test unitaire .

Ainsi, vous pourriez dire que tant que vous ne cassez aucun de vos tests unitaires, vous refactorisez . Lorsque vous passez des tests, vous ne refactorisez plus.

Mais: que diriez-vous de ces refactorings simples tels que le changement des noms de classes ou de membres? Ne cassent-ils pas les tests?

Oui, et dans chaque cas, vous devrez déterminer si cette rupture est importante. Si votre SUT est une API / SDK publique, un changement de nom est en effet un changement de rupture. Sinon, c'est probablement OK.

Cependant, considérez que souvent, les tests échouent non pas parce que vous avez changé le comportement qui vous tient vraiment à cœur, mais parce que les tests sont des tests fragiles .


3

La meilleure façon de définir un «comportement externe», dans ce contexte, peut être des «cas de test».

Si vous refactorisez le code et qu'il continue de passer les cas de test (définis avant le refactoring), le refactoring n'a pas modifié le comportement externe. Si un ou plusieurs cas de test échouent, vous avez modifié le comportement externe.

C'est du moins ce que je comprends des divers livres publiés sur le sujet (par exemple, Fowler's).


2

La frontière serait la ligne qui indique entre ceux qui développent, maintiennent, soutiennent le projet et ceux qui en sont les utilisateurs autres que les supporters, les mainteneurs, les développeurs. Donc, pour le monde extérieur, le comportement est le même alors que les structures internes derrière le comportement ont changé.

Il devrait donc être OK de déplacer des fonctions entre les classes tant qu'elles ne sont pas celles que les utilisateurs voient.

Tant que le code retravaillé ne change pas les comportements externes, n'ajoute pas de nouvelles fonctions ou ne supprime pas les fonctions existantes, je suppose qu'il est OK d'appeler le retravail un refactoring.


Être en désaccord. Si vous remplacez l'intégralité de votre code d'accès aux données par nHibernate, cela ne change pas le comportement externe, mais il ne suit pas les «techniques disciplinées» de Fowler. Ce serait une réingénierie et l'appeler refactoring cache le facteur de risque impliqué.
pdr

1

Avec tout le respect que je vous dois, nous devons nous rappeler que les utilisateurs d'une classe ne sont pas les utilisateurs finaux des applications qui sont construites avec la classe, mais plutôt les classes qui sont implémentées en utilisant - soit en appelant soit en héritant de - la classe en cours de refactorisation. .

Lorsque vous dites que "le comportement externe ne doit pas changer", vous voulez dire qu'en ce qui concerne les utilisateurs, la classe se comporte exactement comme avant. Il se peut que l'implémentation d'origine (non refactorisée) soit une seule classe et que la nouvelle implémentation (refactorisée) ait une ou plusieurs super-classes sur lesquelles la classe est construite, mais les utilisateurs ne voient jamais l'intérieur (l'implémentation) qu'ils voir uniquement l'interface.

Donc, si une classe a une méthode appelée "doSomethingAmazing", cela n'a pas d'importance pour l'utilisateur si cela est implémenté par la classe à laquelle ils se réfèrent, ou par une superclasse sur laquelle cette classe est construite. Tout ce qui compte pour l'utilisateur, c'est que le nouveau (refactorisé) "doSomethingAmazing" a le même résultat que l'ancien (non refactoré) "doSomethingAmazing.

Cependant, ce que l'on appelle le refactoring dans de nombreux cas n'est pas un vrai refactoring, mais peut-être une réimplémentation qui est effectuée pour rendre le code plus facile à modifier pour ajouter une nouvelle fonctionnalité. Donc, dans ce dernier cas de (pseudo) refactoring, le nouveau code (refactoré) fait en fait quelque chose de différent, ou peut-être quelque chose de plus que l'ancien.


Que faire si un formulaire Windows sert à faire apparaître une boîte de dialogue "Êtes-vous sûr de vouloir appuyer sur le bouton OK?" et j'ai décidé de le supprimer car il fait peu de bien et agace les utilisateurs, alors ai-je refactorisé le code, le remodelé, modifié, dé-buggé, autre?
Job

@job: vous avez modifié le programme pour répondre aux nouvelles spécifications.
jmoreno

À mon humble avis, vous pouvez mélanger différents critères ici. Réécrire du code à partir de zéro n'est en effet pas une refactorisation, mais il en est ainsi, qu'il modifie ou non le comportement externe. De plus, si changer une interface de classe n'était pas une refactorisation, comment se fait-il que Move Method et al. existe-t-il dans le catalogue des refactorings ?
Péter Török

@ Péter Török - Cela dépend entièrement de ce que vous entendez par "changer une interface de classe" car dans un langage OOP qui implémente l'héritage, l'interface d'une classe comprend non seulement ce qui est implémenté par la classe elle-même par tous ses ancêtres. Changer une interface de classe signifie supprimer / ajouter une méthode à l'interface (ou changer la signature d'une méthode - c'est-à-dire le nombre d'un type de paramètres qui sont passés). La refactorisation signifie qui répond à la méthode, à la classe ou à une superclasse.
Zeke Hansell

À mon humble avis - toute cette question peut être trop ésotérique pour avoir une valeur utile pour les programmeurs.
Zeke Hansell

1

Par «comportement externe», il parle principalement de l'interface publique, mais cela englobe également les sorties / artefacts du système. (c'est-à-dire que vous avez une méthode qui génère un fichier, changer le format du fichier changerait le comportement externe)

e: Je considérerais la "méthode de déplacement" comme un changement de comportement externe. Gardez à l'esprit ici que Fowler parle de bases de code existantes qui ont été publiées dans la nature. Selon votre situation, vous pourrez peut-être vérifier que votre changement ne casse aucun client externe et poursuivre votre joyeux chemin.

e2: "Alors, quel est le bon mot pour des retouches qui changent le comportement externe?" - Refactoring API, Breaking Change, etc ... son refactoring toujours, il ne suit tout simplement pas les meilleures pratiques pour refactoring une interface publique qui est déjà dans la nature avec les clients.


Mais s'il parle d'une interface publique, qu'en est-il du refactoring "Move method"?
SiberianGuy

@Idsa a modifié ma réponse pour répondre à votre question.

@kekela, mais je ne sais toujours pas où finit ce truc de "refactoring"
SiberianGuy

@idsa Selon la définition que vous avez publiée, elle cesse d'être une refactorisation à la minute où vous changez une interface publique. (le déplacement d'une méthode publique d'une classe à une autre en serait un exemple)

0

La «méthode de déplacement» est une technique de refactorisation, pas une refactorisation en soi. Un refactoring est le processus d'application de plusieurs techniques de refactoring aux classes. Là, quand vous dites, j'ai appliqué la "méthode de déplacement" à une classe, vous ne voulez pas dire "j'ai refactorisé (la classe)", vous voulez dire "j'ai appliqué une technique de refactoring sur cette classe". La refactorisation, dans son sens le plus pur, est appliquée à la conception, ou plus spécifiquement, à une partie de la conception d'une application qui peut être considérée comme une boîte noire.

On pourrait dire que "refactoring" utilisé dans le contexte des classes, signifie "technique de refactoring", donc "méthode de déplacement" ne rompt pas la définition de refactoring-le-processus. Sur la même page, le «refactoring» dans le contexte du design, ne casse pas les fonctionnalités existantes dans le code, il «casse» seulement le design (ce qui est de toute façon son but).

En conclusion, "la frontière", mentionnée dans la question, est franchie si vous confondez (mix: D) le refactoring-la-technique avec le refactoring-du-processus.

PS: Bonne question, au fait.


Lisez-le plusieurs fois, mais ne le comprenez toujours pas
SiberianGuy

quelle partie n'avez-vous pas comprise? (il y a la refactorisation-du-processus à laquelle votre définition s'applique ou la refactorisation-de-la technique, dans votre exemple, méthode de déplacement, à laquelle la définition ne s'applique pas; par conséquent, la méthode de déplacement ne rompt pas la définition de refactorisation-du- processus, ou ne franchit pas ses frontières, quelles qu'elles soient). je dis que la préoccupation que vous avez ne devrait pas exister pour votre exemple. la frontière du refactoring n'est pas quelque chose de flou. vous appliquez simplement une définition de quelque chose à autre chose.
Belun

0

si vous parlez de la factorisation des nombres, vous décrivez le groupe d'entiers qui, multipliés ensemble, sont égaux au nombre de départ. Si nous prenons cette définition pour l'affacturage et l'appliquons au terme de programmation refactoriser, alors le refactoring serait de décomposer un programme en plus petites unités logiques, de sorte que lorsqu'elles sont exécutées en tant que programme, produire la même sortie (avec la même entrée ) comme programme d'origine.


0

Les limites d'un refactoring sont spécifiques à un refactoring donné. Il ne peut tout simplement pas y avoir une seule réponse et englobe tout. L'une des raisons est que le terme refactoring est lui-même non spécifique.

Vous pouvez refactoriser:

  • Code source
  • Architecture du programme
  • Conception d'interface utilisateur / interaction utilisateur

et je suis sûr que plusieurs autres choses.

Le refactoriseur pourrait peut-être être défini comme

"une action ou un ensemble d'actions qui diminuent l'entropie dans un système particulier"


0

Il y a certainement des limites à la mesure dans laquelle vous pouvez refactoriser. Voir: Quand deux algorithmes sont-ils identiques?

Les gens considèrent généralement les algorithmes comme plus abstraits que les programmes qui les mettent en œuvre. La manière naturelle de formaliser cette idée est que les algorithmes sont des classes d'équivalence de programmes par rapport à une relation d'équivalence appropriée. Nous soutenons qu'aucune relation d'équivalence n'existe.

Alors n'allez pas trop loin, sinon vous n'aurez aucune confiance dans le résultat. D'un autre côté, l'expérience dicte que vous pouvez souvent remplacer un algorithme par un autre et obtenir les mêmes réponses, parfois plus rapidement. C'est ça la beauté, hein?


0

Vous pouvez penser à un sens «externe»

Extérieur au stand up quotidien.

Ainsi, toute modification du système qui n'affecte aucun porc peut être considérée comme une refactorisation.

Changer une interface de classe n'est pas un problème, si la classe n'est utilisée que par un seul système qui est construit et maintenu par votre équipe. Cependant, si la classe est une classe publique dans le framework .net utilisé par tous les programmeurs .net, c'est une question très différente.

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.