JavaRush /Blog Java /Random-FR /Comment fonctionne la refactorisation en Java

Comment fonctionne la refactorisation en Java

Publié dans le groupe Random-FR
Lorsqu’on apprend à programmer, on passe beaucoup de temps à écrire du code. La plupart des développeurs débutants pensent qu'il s'agit de leur activité future. C’est en partie vrai, mais les tâches d’un programmeur incluent également la maintenance et la refactorisation du code. Aujourd'hui, nous allons parler de refactoring. Comment fonctionne le refactoring en Java - 1

Refactoring dans le cours JavaRush

Le cours JavaRush couvre le sujet du refactoring à deux reprises : Grâce à la grande tâche, il est possible de se familiariser avec le véritable refactoring dans la pratique, et une conférence sur le refactoring dans IDEA vous aidera à comprendre les outils automatiques qui rendent la vie incroyablement plus facile.

Qu’est-ce que la refactorisation ?

Il s'agit d'un changement dans la structure du code sans changer ses fonctionnalités. Par exemple, il existe une méthode qui compare 2 nombres et renvoie vrai si le premier est plus grand, et faux sinon :

   public boolean max(int a, int b) {
       if(a > b) {
           return true;
       } else if(a == b) {
           return false;
       } else {
           return false;
       }
   }
Le résultat était un code très lourd. Même les débutants écrivent rarement quelque chose comme ça, mais il y a un tel risque. Il semblerait, pourquoi y a-t-il un bloc ici if-elsesi vous pouvez écrire une méthode 6 lignes plus courte :

public boolean max(int a, int b) {
     return a>b;
}
Cette méthode semble désormais simple et élégante, même si elle fait la même chose que l’exemple ci-dessus. C'est ainsi que fonctionne le refactoring : il modifie la structure du code sans affecter son essence. Il existe de nombreuses méthodes et techniques de refactoring, que nous examinerons plus en détail.

Pourquoi une refactorisation est-elle nécessaire ?

Il existe plusieurs raisons. Par exemple, la recherche de la simplicité et de la concision du code. Les partisans de cette théorie estiment que le code doit être aussi concis que possible, même si sa compréhension nécessite des dizaines de lignes de commentaires. D'autres développeurs estiment que le code devrait être remanié afin qu'il soit compréhensible avec un minimum de commentaires. Chaque équipe choisit sa propre position, mais il faut se rappeler que le refactoring n'est pas une réduction . Son objectif principal est d'améliorer la structure du code. Plusieurs objectifs peuvent être inclus dans cet objectif global :
  1. La refactorisation améliore la compréhension du code écrit par un autre développeur ;
  2. Aide à trouver et à corriger les erreurs ;
  3. Vous permet d'augmenter la vitesse de développement de logiciels ;
  4. Améliore globalement la composition du logiciel.
Si le refactoring n'est pas effectué pendant une longue période, des difficultés de développement peuvent survenir, jusqu'à un arrêt complet des travaux.

« Le code sent »

Lorsque le code nécessite une refactorisation, ils disent que cela « sent ». Bien sûr, pas littéralement, mais un tel code n’a vraiment pas l’air très joli. Ci-dessous, nous examinerons les principales techniques de refactoring pour la phase initiale.

Éléments inutilement volumineux

Il existe des classes et des méthodes lourdes avec lesquelles il est impossible de travailler efficacement, précisément en raison de leur taille énorme.

Grande classe

Une telle classe possède un grand nombre de lignes de code et de nombreuses méthodes différentes. Il est généralement plus facile pour un développeur d’ajouter une fonctionnalité à une classe existante plutôt que d’en créer une nouvelle, c’est pourquoi elle se développe. En règle générale, les fonctionnalités de cette classe sont surchargées. Dans ce cas, il est utile de séparer une partie des fonctionnalités dans une classe distincte. Nous en parlerons plus en détail dans la section techniques de refactoring.

Grande méthode

Cette « odeur » se produit lorsqu'un développeur ajoute une nouvelle fonctionnalité à une méthode. "Pourquoi devrais-je mettre la vérification des paramètres dans une méthode distincte si je peux l'écrire ici ?", "Pourquoi est-il nécessaire de séparer la méthode pour trouver l'élément maximum dans le tableau, laissons cela ici. De cette façon, le code est plus clair », et d'autres idées fausses. Il existe deux règles pour refactoriser une grande méthode :
  1. Si, lors de l'écriture d'une méthode, vous souhaitez ajouter un commentaire au code, vous devez séparer cette fonctionnalité en une méthode distincte ;
  2. Si une méthode prend plus de 10 à 15 lignes de code, vous devez identifier les tâches et sous-tâches qu'elle effectue et essayer de séparer les sous-tâches en une méthode distincte.
Plusieurs façons d'éliminer une méthode volumineuse :
  • Séparer une partie des fonctionnalités d’une méthode en une méthode distincte ;
  • Si les variables locales ne permettent pas d'extraire une partie de la fonctionnalité, vous pouvez passer l'intégralité de l'objet à une autre méthode.

Utilisation de nombreux types de données primitifs

En règle générale, ce problème se produit lorsque le nombre de champs permettant de stocker des données dans une classe augmente avec le temps. Par exemple, si vous utilisez des types primitifs au lieu de petits objets pour stocker des données (devise, date, numéros de téléphone, etc.) ou des constantes pour coder des informations. Une bonne pratique dans ce cas serait de regrouper logiquement les champs et de les placer dans une classe distincte (en sélectionnant une classe). Vous pouvez également inclure des méthodes de traitement de ces données dans la classe.

Longue liste d'options

Une erreur assez courante, surtout en combinaison avec une méthode large. Cela se produit généralement si la fonctionnalité de la méthode est surchargée ou si la méthode combine plusieurs algorithmes. Les longues listes de paramètres sont très difficiles à comprendre et ces méthodes sont peu pratiques à utiliser. Par conséquent, il est préférable de transférer l’intégralité de l’objet. Si l'objet ne contient pas suffisamment de données, il vaut la peine d'utiliser un objet plus général ou de diviser les fonctionnalités de la méthode afin qu'elle traite des données logiquement liées.

Groupes de données

Des groupes de données logiquement liés apparaissent souvent dans le code. Par exemple, les paramètres de connexion à la base de données (URL, nom d'utilisateur, mot de passe, nom de schéma, etc.). Si aucun champ ne peut être supprimé de la liste d’éléments, alors la liste est un groupe de données qui doit être placé dans une classe distincte (sélection de classe).

Des solutions qui gâchent le concept de POO

Ce type « d'odeur » se produit lorsque le développeur viole la conception de la POO. Cela se produit s'il ne comprend pas pleinement les capacités de ce paradigme, s'il les utilise de manière incomplète ou incorrecte.

Refus d'héritage

Si une sous-classe utilise une partie minimale des fonctions de la classe parent, cela ressemble à une hiérarchie incorrecte. En règle générale, dans ce cas, les méthodes inutiles ne sont tout simplement pas remplacées ou des exceptions sont levées. Si une classe est héritée d’une autre, cela implique une utilisation presque complète de ses fonctionnalités. Exemple de hiérarchie correcte : Comment fonctionne le refactoring en Java - 2 Exemple de hiérarchie incorrecte : Comment fonctionne le refactoring en Java - 3

instruction de commutation

Qu'est-ce qui pourrait clocher chez un opérateur switch? C'est mauvais quand sa conception est très complexe. Cela inclut également de nombreux blocs imbriqués if.

Classes alternatives avec différentes interfaces

Plusieurs classes font en réalité la même chose, mais leurs méthodes sont nommées différemment.

Champ temporaire

Si la classe contient un champ temporaire dont l'objet n'a besoin qu'occasionnellement, lorsqu'il est rempli de valeurs, et le reste du temps il est vide ou, Dieu nous en préserve, nullalors le code « sent », et une telle conception est douteuse décision.

Odeurs qui rendent la modification difficile

Ces « odeurs » sont plus graves. Le reste altère principalement la compréhension du code, alors que ceux-ci ne permettent pas de le modifier. Lors de l'introduction de fonctionnalités, la moitié des développeurs quitteront et l'autre moitié deviendra fou.

Hiérarchies d'héritage parallèles

Lorsque vous créez une sous-classe d'une classe, vous devez créer une autre sous-classe pour une autre classe.

Répartition uniforme des dépendances

Lors de toute modification, vous devez rechercher toutes les dépendances (utilisations) de cette classe et apporter de nombreuses petites modifications. Un changement : modifications dans de nombreuses classes.

Arbre de modification complexe

Cette odeur est à l’opposé de la précédente : les changements affectent un grand nombre de méthodes d’une même classe. En règle générale, la dépendance dans un tel code est en cascade : après avoir modifié une méthode, vous devez corriger quelque chose dans une autre, puis dans une troisième, et ainsi de suite. Une classe - de nombreux changements.

« Des odeurs de déchets »

Une catégorie d'odeurs plutôt désagréables qui provoque des maux de tête. Vieux code inutile, inutile. Heureusement, les IDE et les linters modernes ont appris à avertir de telles odeurs.

Un grand nombre de commentaires dans la méthode

La méthode comporte de nombreux commentaires explicatifs sur presque chaque ligne. Ceci est généralement associé à un algorithme complexe, il est donc préférable de diviser le code en plusieurs méthodes plus petites et de leur donner des noms significatifs.

Duplication de codes

Différentes classes ou méthodes utilisent les mêmes blocs de code.

Cours paresseux

La classe assume très peu de fonctionnalités, même si une grande partie était prévue.

Code inutilisé

Une classe, une méthode ou une variable n’est pas utilisée dans le code et constitue un « poids mort ».

Couplage excessif

Cette catégorie d'odeurs se caractérise par un grand nombre de connexions inutiles dans le code.

Méthodes tierces

Une méthode utilise les données d’un autre objet beaucoup plus souvent que ses propres données.

Intimité inappropriée

Une classe utilise les champs de service et les méthodes d'une autre classe.

Appels longue classe

Une classe en appelle une autre, qui demande des données à la troisième, celles à la quatrième, et ainsi de suite. Une chaîne d’appels aussi longue signifie un niveau élevé de dépendance à l’égard de la structure de classe actuelle.

Concessionnaire de tâches de classe

Une classe n’est nécessaire que pour transmettre une tâche à une autre classe. Peut-être faudrait-il le supprimer ?

Techniques de refactorisation

Ci-dessous, nous parlerons des techniques de refactorisation initiales qui aideront à éliminer les odeurs de code décrites.

Sélection de classe

La classe remplit trop de fonctions ; certaines d'entre elles doivent être déplacées vers une autre classe. Par exemple, il existe une classe Humanqui contient également une adresse résidentielle et une méthode qui fournit l'adresse complète :

class Human {
   private String name;
   private String age;
   private String country;
   private String city;
   private String street;
   private String house;
   private String quarter;

   public String getFullAddress() {
       StringBuilder result = new StringBuilder();
       return result
                       .append(country)
                       .append(", ")
                       .append(city)
                       .append(", ")
                       .append(street)
                       .append(", ")
                       .append(house)
                       .append(" ")
                       .append(quarter).toString();
   }
}
Ce serait une bonne idée de placer les informations d'adresse et la méthode (comportement du traitement des données) dans une classe distincte :

class Human {
   private String name;
   private String age;
   private Address address;

   private String getFullAddress() {
       return address.getFullAddress();
   }
}
class Address {
   private String country;
   private String city;
   private String street;
   private String house;
   private String quarter;

   public String getFullAddress() {
       StringBuilder result = new StringBuilder();
       return result
                       .append(country)
                       .append(", ")
                       .append(city)
                       .append(", ")
                       .append(street)
                       .append(", ")
                       .append(house)
                       .append(" ")
                       .append(quarter).toString();
   }
}

Sélection de la méthode

Si une fonctionnalité d’une méthode peut être regroupée, elle doit être placée dans une méthode distincte. Par exemple, une méthode qui calcule les racines d'une équation quadratique :

   public void calcQuadraticEq(double a, double b, double c) {
       double D = b * b - 4 * a * c;
       if (D > 0) {
           double x1, x2;
           x1 = (-b - Math.sqrt(D)) / (2 * a);
           x2 = (-b + Math.sqrt(D)) / (2 * a);
           System.out.println("x1 = " + x1 + ", x2 = " + x2);
       }
       else if (D == 0) {
           double x;
           x = -b / (2 * a);
           System.out.println("x = " + x);
       }
       else {
           System.out.println("Equation has no roots");
       }
   }
Déplaçons le calcul des trois options possibles dans des méthodes distinctes :

   public void calcQuadraticEq(double a, double b, double c) {
       double D = b * b - 4 * a * c;
       if (D > 0) {
           dGreaterThanZero(a, b, D);
       }
       else if (D == 0) {
           dEqualsZero(a, b);
       }
       else {
           dLessThanZero();
       }
   }

   public void dGreaterThanZero(double a, double b, double D) {
       double x1, x2;
       x1 = (-b - Math.sqrt(D)) / (2 * a);
       x2 = (-b + Math.sqrt(D)) / (2 * a);
       System.out.println("x1 = " + x1 + ", x2 = " + x2);
   }

   public void dEqualsZero(double a, double b) {
       double x;
       x = -b / (2 * a);
       System.out.println("x = " + x);
   }

   public void dLessThanZero() {
       System.out.println("Equation has no roots");
   }
Le code de chaque méthode est devenu beaucoup plus court et plus clair.

Transférer l'intégralité de l'objet

Lorsque vous appelez une méthode avec des paramètres, vous pouvez parfois voir du code comme celui-ci :

public void employeeMethod(Employee employee) {
    // Некоторые действия
    double yearlySalary = employee.getYearlySalary();
    double awards = employee.getAwards();
    double monthlySalary = getMonthlySalary(yearlySalary, awards);
    // Продолжение обработки
}

public double getMonthlySalary(double yearlySalary, double awards) {
     return (yearlySalary + awards)/12;
}
Dans la méthode, employeeMethodjusqu'à 2 lignes sont allouées pour obtenir des valeurs et les stocker dans des variables primitives. Parfois, ces conceptions prennent jusqu'à 10 lignes. Il est beaucoup plus simple de transmettre l'objet lui-même à la méthode, d'où vous pouvez extraire les données nécessaires :

public void employeeMethod(Employee employee) {
    // Некоторые действия
    double monthlySalary = getMonthlySalary(employee);
    // Продолжение обработки
}

public double getMonthlySalary(Employee employee) {
    return (employee.getYearlySalary() + employee.getAwards())/12;
}
Simple, court et concis.

Regroupement logique des champs et placement dans une classe distincte

Malgré le fait que les exemples ci-dessus sont très simples et qu'en les regardant, beaucoup peuvent se poser la question « Qui fait réellement ça ? », de nombreux développeurs, par inattention, par manque de volonté de refactoriser le code, ou simplement par « Cela fera l'affaire », font l'affaire. erreurs structurelles similaires.

Pourquoi la refactorisation est efficace

Le résultat d'une bonne refactorisation est un programme dont le code est facile à lire, les modifications de la logique du programme ne deviennent pas une menace et l'introduction de nouvelles fonctionnalités ne se transforme pas en un enfer d'analyse de code, mais en une activité agréable pendant quelques jours. . La refactorisation ne doit pas être utilisée s'il est plus facile de réécrire le programme à partir de zéro. Par exemple, l’équipe estime que les coûts de main-d’œuvre pour analyser, analyser et refactoriser le code sont plus élevés que pour implémenter la même fonctionnalité à partir de zéro. Ou le code qui doit être refactorisé comporte de nombreuses erreurs difficiles à déboguer. Savoir comment améliorer la structure du code est obligatoire dans le travail d’un programmeur. Eh bien, il est préférable d'apprendre la programmation Java sur JavaRush - un cours en ligne mettant l'accent sur la pratique. Plus de 1 200 tâches avec vérification instantanée, environ 20 mini-projets, tâches de jeu - tout cela vous aidera à vous sentir en confiance dans le codage. Le meilleur moment pour commencer est maintenant :) Comment fonctionne le refactoring en Java - 4

Ressources pour approfondir la refactorisation

Le livre le plus célèbre sur le refactoring est « Refactoring ». Améliorer la conception du code existant »par Martin Fowler. Il existe également une publication intéressante sur le refactoring, écrite sur la base d'un livre précédent - « Refactoring with Patterns » de Joshua Kiriewski. En parlant de modèles. Lors d’une refactorisation, il est toujours très utile de connaître les modèles de conception de base des applications. Ces excellents livres vous aideront à cela :
  1. « Design Patterns » - par Eric Freeman, Elizabeth Freeman, Kathy Sierra, Bert Bates de la série Head First ;
  2. "Code lisible ou programmation comme art" - Dustin Boswell, Trevor Faucher.
  3. « Perfect Code » de Steve McConnell, qui décrit les principes d'un code beau et élégant.
Eh bien, quelques articles sur le refactoring :
  1. Une sacrée tâche : commençons à refactoriser le code existant ;
  2. Refactorisation ;
  3. La refactorisation pour tout le monde .
    Commentaires
    TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
    GO TO FULL VERSION