Question

J'ai eu l'habitude de refactoriser en introduisant des erreurs de compilation. Par exemple, si je veux supprimer un champ de ma classe et en faire un paramètre pour certaines méthodes, je supprime généralement le champ en premier, ce qui provoque une erreur de compilation pour la classe. Ensuite, je voudrais introduire le paramètre à mes méthodes, ce qui permettrait de rompre les appelants. Etc. Cela me donnait généralement un sentiment de sécurité. Je n'ai pas encore lu de livre sur la refactorisation, mais je pensais que c'était un moyen relativement sûr de le faire. Mais je me demande, est-ce vraiment sûr? Ou est-ce une mauvaise façon de faire les choses?

Était-ce utile?

La solution

C’est une technique courante et utile pour les langages compilés statiquement. La version générale de ce que vous faites peut être énoncée comme suit:

  

Lorsque vous apportez une modification à un module susceptible d'invalider certaines utilisations des clients de ce module, apportez la modification initiale de manière à provoquer une erreur de compilation.

Il existe une variété de corollaires:

  • Si la signification d'une méthode, d'une fonction ou d'une procédure change et que le type ne change pas également, changez le nom. (Une fois que vous aurez examiné et corrigé toutes les utilisations, vous changerez probablement le nom.)

  • Si vous ajoutez un nouveau cas à un type de données ou un nouveau littéral à une énumération, modifiez les noms de tous les constructeurs de types de données ou littéraux d'énumération existants. (Ou, si vous avez la chance d'avoir un compilateur qui vérifie si l'analyse de cas est exhaustive, il existe des moyens plus simples.)

  • Si vous travaillez dans une langue surchargée, ne ne changez pas une variante ou ajoutez-en une nouvelle. Vous risquez que la surcharge résolve de manière silencieuse d'une manière différente. Si vous utilisez la surcharge, il est assez difficile de faire en sorte que le compilateur fonctionne pour vous comme vous le souhaitez. Le seul moyen que je connaisse pour gérer la surcharge est de raisonner globalement sur toutes les utilisations. Si votre IDE ne vous aide pas, vous devez modifier le nom de toutes les variantes surchargées. Désagréable.

Ce que vous faites réellement, c'est d'utiliser le compilateur pour vous aider à examiner tous les endroits du code que vous pourriez avoir besoin de changer.

Autres conseils

Je ne me fie jamais à une simple compilation lors du refactoring, le code peut être compilé mais des bugs ont peut-être été introduits.

Je pense que l'écriture de certains tests unitaires pour les méthodes ou les classes que vous souhaitez refactoriser sera la meilleure solution. Ensuite, en exécutant le test après le refactoring, vous serez sûr qu'aucun bogue n'a été introduit.

Je ne dis pas aller dans Test Driven Development, écrivez simplement les tests unitaires pour obtenir l'assurance nécessaire à la refactorisation.

Je ne vois pas de problème avec ça. C'est sûr, et tant que vous n'effectuez aucune modification avant de compiler, cela n'a aucun effet à long terme. De plus, Resharper et VS disposent d’outils qui facilitent un peu le processus.

Vous utilisez un processus similaire dans l’autre sens de TDD: vous écrivez du code dont les méthodes ne sont pas définies, ce qui l’empêche de compiler, puis vous écrivez suffisamment de code pour compiler (puis passez des tests, etc.). .)

Lorsque vous êtes prêt à lire des livres sur le sujet, je vous recommande de & ranger de Michael Feather. Travailler efficacement avec le code existant " ;. ( Ajouté par non-auteur: également le livre classique de Fowler " Refactoring " - et le site Web Refactoring peuvent être utiles. )

Il parle d’identifier les caractéristiques du code sur lequel vous travaillez avant de faire un changement et de faire ce qu’il appelle du refactoring de travail. C’est réévaluer pour trouver les caractéristiques du code et ensuite jeter les résultats.

Vous utilisez le compilateur comme test automatique. Il vérifiera que votre code est compilé, mais pas si le comportement a changé en raison de votre refactoring ou s'il y a eu des effets secondaires.

Considérez ceci

class myClass {
     void megaMethod() 
     {
         int x,y,z;
         //lots of lines of code
         z = mysideEffect(x)+y;
         //lots more lines of code 
         a = b + c;
     }
}

vous pouvez refactorer l'addition

class myClass {
     void megaMethod() 
     {
         int a,b,c,x,y,z;
         //lots of lines of code
         z = addition(x,y);
         //lots more lines of code
         a = addition(b,c);  
     }

     int addition(int a, b)
     {
          return mysideaffect(a)+b;
     }
}

et cela fonctionnerait, mais le second additon serait faux car il invoquerait la méthode. Des tests supplémentaires seraient nécessaires en plus de la compilation.

Il est assez facile de penser à un exemple où le refactoring par des erreurs de compilation échoue en silence et produit des résultats inattendus.
Quelques cas qui me viennent à l’esprit: (je suppose que nous parlons de C ++)

  • Modification des arguments en une fonction où d’autres surcharges existent avec des paramètres par défaut. Après le refactoring, il se peut que la meilleure correspondance pour les arguments ne corresponde pas à vos attentes.
  • Les classes qui ont des opérateurs de casting ou des constructeurs à un seul argument non explicites. La modification, l’ajout, la suppression ou la modification des arguments de ces éléments peut modifier les meilleures correspondances appelées, en fonction de la constellation concernée.
  • Changer une fonction virtuelle sans changer la classe de base (ou changer la classe de base différemment) aura pour conséquence que les appels seront dirigés vers la classe de base.

Il est recommandé d’utiliser les erreurs du compilateur uniquement si vous êtes absolument certain que le compilateur détectera chaque modification à effectuer. J'ai presque toujours tendance à avoir des soupçons à ce sujet.

Je voudrais juste ajouter à toute la sagesse qui règne ici qu’il existe un cas de plus où cela pourrait ne pas être sûr. Réflexion. Cela affecte des environnements tels que .NET et Java (et d'autres, bien sûr). Votre code serait compilé, mais il resterait des erreurs d’exécution lorsque Reflection essaierait d’accéder à des variables inexistantes. Par exemple, cela pourrait être assez courant si vous utilisez un ORM comme Hibernate et oubliez de mettre à jour vos fichiers XML de mappage.

Il pourrait être un peu plus sûr de rechercher dans tous les fichiers de code le nom de variable / méthode spécifique. Bien sûr, cela pourrait évoquer beaucoup de faux positifs, alors ce n’est pas une solution universelle; et vous pourriez aussi utiliser la concaténation de chaînes dans votre réflexion, ce qui la rendrait également inutile. Mais c’est au moins un petit pas de plus vers la sécurité.

Je ne pense pas qu'il existe une méthode 100% infaillible, mis à part le fait de lire tout le code manuellement.

C’est une façon assez commune, je pense, de trouver toutes les références à cette chose spécifique Cependant, les IDE modernes tels que Visual Studio ont la fonction Rechercher toutes les références qui rend cela inutile.

Cependant, cette approche présente certains inconvénients. Pour les grands projets, la compilation de l'application peut prendre beaucoup de temps. De plus, ne le faites pas pendant longtemps (je veux dire, remettez les choses au travail dès que possible) et ne le faites pas plus d’une chose à la fois, car vous pourriez oublier la bonne façon de corriger les choses la première fois.

C'est un moyen et il ne peut y avoir aucune déclaration précise quant à savoir si c'est sûr ou non, sans savoir à quoi ressemble le code que vous refactoring et les choix que vous faites.

Si cela fonctionne pour vous, il n'y a aucune raison de changer, rien que pour changer, mais quand vous avez le temps de lire, les ressources ici peuvent vous donner de nouvelles idées que vous voudrez peut-être explorer au fil du temps.

http://www.refactoring.com/

Si vous utilisez l'une des langues dotnet, vous pouvez également envisager de marquer le " ancien " méthode avec l'attribut Obsolete, qui introduira tous les avertissements du compilateur, tout en laissant le code appelable s'il devait y avoir du code incontrôlable (si vous écrivez une API, ou si vous n'utilisez pas l'option strict dans VB.Net, par exemple). Vous pouvez alors gaiement refactoriser, ayant la version obsolète appeler la nouvelle; à titre d'exemple:

    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    public int Login()
    {
        /* do stuff */
    }

devient:

    [ObsoleteAttribute()]
    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    [ObsoleteAttribute("Replaced by Login(username, password)")]
    public int Login()
    {
        Login(Username, Pasword);
    }

    public int Login(string username, string password)
    {
        /* do stuff */
    }

C’est comme ça que j’ai tendance à le faire, de toute façon ...

Il s'agit d'une approche courante, mais les résultats peuvent varier selon que votre langage soit statique ou dynamique.

Dans un langage à typage statique, cette approche a du sens, car toute divergence que vous introduisez sera détectée au moment de la compilation. Cependant, les langages dynamiques ne rencontrent souvent ces problèmes qu’au moment de l’exécution. Ces problèmes ne seraient pas capturés par le compilateur, mais par votre suite de tests; en supposant que vous en avez écrit un.

J'ai l'impression que vous travaillez avec un langage statique tel que C # ou Java. Continuez donc avec cette approche jusqu'à ce que vous rencontriez un problème majeur qui vous oblige à faire autrement.

Je fais des refactorisations habituelles, mais je le fais quand même en introduisant des erreurs de compilation. Je les fais généralement lorsque les modifications ne sont pas si simples et que cette refactorisation n'est pas une refactorisation réelle (je change de fonctionnalité). Ces erreurs de compilateur me donnent des tâches que je dois examiner et effectuer des modifications plus compliquées que celles des noms ou des paramètres.

Cela ressemble à une méthode absolument standard utilisée dans le développement piloté par les tests: écrivez le test en vous référant à une classe non existante, de sorte que la première étape pour réussir le test consiste à ajouter la classe, puis les méthodes, etc. . Consultez le le livre de Beck pour des exemples exhaustifs en Java.

Votre méthode de refactoring semble dangereuse, car vous n’avez aucun test de sécurité (ou du moins vous n’indiquez pas que vous en avez). Vous pouvez créer un code de compilation qui ne fait pas ce que vous voulez ou qui casse d'autres parties de votre application.

Je vous suggérerais d’ajouter une règle simple à votre pratique: effectuez des modifications de non-compilation uniquement dans le code de test unitaire . De cette façon, vous êtes assuré de disposer au moins d’un test local pour chaque modification, et vous enregistrez l’intention de la modification dans votre test avant de l’effectuer.

À propos, Eclipse fait en sorte que "échec, stub, écrit" méthode absurdement facile en Java: chaque objet non existant est marqué pour vous, et Ctrl-1 plus un choix de menu indiquent à Eclipse d'écrire un talon (compilable) pour vous! Je serais intéressé de savoir si d'autres langues et IDE fournissent un support similaire.

C'est "sûr" en ce sens que dans un langage dont le temps de compilation est suffisamment long, cela vous oblige à mettre à jour toutes les références réelles à la chose que vous avez modifiée.

Cela peut toujours mal tourner si vous avez un code compilé sous condition, par exemple si vous avez utilisé le préprocesseur C / C ++. Assurez-vous donc de reconstruire dans toutes les configurations possibles et sur toutes les plateformes, le cas échéant.

Cela ne supprime pas la nécessité de tester vos modifications. Si vous avez ajouté un paramètre à une fonction, le compilateur ne peut pas vous dire si la valeur que vous avez fournie lors de la mise à jour de chaque site d'appels pour cette fonction était la bonne valeur. Si vous avez supprimé un paramètre, vous pouvez toujours faire des erreurs, par exemple, modifiez:

void foo(int a, int b);

à

void foo(int a);

Modifiez ensuite l'appel depuis:

foo(1,2);

à:

foo(2);

Ça compile bien, mais c'est faux.

Personnellement, j’utilise les échecs de compilation (et de lien) comme moyen de rechercher dans le code des références réelles à la fonction que je change. Mais vous devez garder à l'esprit qu'il ne s'agit que d'un appareil permettant d'économiser du travail. Cela ne garantit pas que le code résultant est correct.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top