Au fur et à mesure que vous apprenez à programmer, vous passez beaucoup de temps à écrire du code. La plupart des développeurs débutants pensent que c'est ce qu'ils feront à l'avenir. C'est en partie vrai, mais le travail d'un programmeur comprend également la maintenance et la refactorisation du code. Aujourd'hui, nous allons parler de refactorisation. Comment fonctionne la refactorisation en Java - 1

Refactoring sur CodeGym

Le refactoring est abordé deux fois dans le cours CodeGym : La grande tâche offre l'occasion de se familiariser avec la vraie refactorisation par la pratique, et la leçon sur la refactorisation dans IDEA vous aide à vous plonger dans des outils automatisés qui vous faciliteront incroyablement la vie.

Qu'est-ce que le refactoring ?

Il change la structure du code sans changer sa fonctionnalité. Par exemple, supposons que nous ayons 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;
        }
    }
C'est un code assez lourd. Même les débutants écriraient rarement quelque chose comme ça, mais il y a une chance. Pourquoi utiliser un if-elsebloc si vous pouvez écrire la méthode en 6 lignes de manière plus concise ?

 public boolean max(int a, int b) {
      return a > b;
 }
Nous avons maintenant une méthode simple et élégante qui effectue la même opération que l'exemple ci-dessus. C'est ainsi que fonctionne le refactoring : vous modifiez la structure du code sans affecter son essence. Il existe de nombreuses méthodes et techniques de refactoring que nous allons examiner de plus près.

Pourquoi avez-vous besoin d'un refactoring ?

Il existe plusieurs raisons. Par exemple, pour atteindre la simplicité et la brièveté du code. Les tenants de cette théorie estiment que le code doit être le plus concis possible, même si plusieurs dizaines de lignes de commentaires sont nécessaires pour le comprendre. D'autres développeurs sont convaincus que le code doit être refactorisé pour le rendre compréhensible avec le minimum de commentaires. Chaque équipe adopte sa propre position, mais rappelez-vous que refactoring ne signifie pas réduction . Son objectif principal est d'améliorer la structure du code. Plusieurs tâches peuvent être incluses dans cet objectif général :
  1. Le refactoring améliore la compréhension du code écrit par d'autres développeurs.
  2. Il aide à trouver et à corriger les bogues.
  3. Il peut accélérer la vitesse de développement de logiciels.
  4. Dans l'ensemble, cela améliore la conception des logiciels.
Si le refactoring n'est pas effectué pendant une longue période, le développement peut rencontrer des difficultés, y compris un arrêt complet des travaux.

"Le code sent bon"

Lorsque le code nécessite une refactorisation, on dit qu'il a une "odeur". Bien sûr, pas littéralement, mais un tel code n'a vraiment pas l'air très attrayant. Ci-dessous, nous allons explorer les techniques de refactorisation de base pour la phase initiale.

Classes et méthodes déraisonnablement grandes

Les classes et les méthodes peuvent être lourdes, impossibles à utiliser efficacement précisément en raison de leur taille énorme.

Grande classe

Une telle classe a 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 la classe se développe. En règle générale, trop de fonctionnalités sont entassées dans une telle classe. Dans ce cas, il est utile de déplacer une partie de la fonctionnalité dans une classe distincte. Nous en parlerons plus en détail dans la section sur les techniques de refactoring.

Méthode longue

Cette « odeur » survient lorsqu'un développeur ajoute une nouvelle fonctionnalité à une méthode : « Pourquoi devrais-je mettre une vérification de paramètre dans une méthode distincte si je peux écrire le code ici ? », « Pourquoi ai-je besoin d'une méthode de recherche distincte pour trouver le maximum élément dans un tableau ? Gardons-le ici. Le code sera plus clair de cette façon", et d'autres idées fausses.

Il existe deux règles pour refactoriser une méthode longue :

  1. Si vous souhaitez ajouter un commentaire lors de l'écriture d'une méthode, vous devez placer la fonctionnalité dans 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 exécute et essayer de placer les sous-tâches dans une méthode distincte.

Il existe plusieurs façons d'éliminer une longue méthode :

  • Déplacer une partie de la fonctionnalité de la méthode dans une méthode distincte
  • Si des variables locales vous empêchent de déplacer une partie de la fonctionnalité, vous pouvez déplacer l'objet entier vers une autre méthode.

Utilisation de nombreux types de données primitifs

Ce problème se produit généralement lorsque le nombre de champs dans une classe augmente avec le temps. Par exemple, si vous stockez tout (devise, date, numéros de téléphone, etc.) dans des types primitifs ou des constantes au lieu de petits objets. Dans ce cas, une bonne pratique consisterait à déplacer un regroupement logique de champs dans une classe distincte (classe d'extraction). Vous pouvez également ajouter des méthodes à la classe pour traiter les données.

Trop de paramètres

C'est une erreur assez courante, surtout en combinaison avec une longue méthode. Habituellement, cela se produit si une méthode a trop de fonctionnalités ou si une méthode implémente plusieurs algorithmes. De longues listes de paramètres sont très difficiles à comprendre, et l'utilisation de méthodes avec de telles listes n'est pas pratique. Par conséquent, il est préférable de passer un objet entier. Si un objet ne contient pas suffisamment de données, vous devez utiliser un objet plus général ou diviser les fonctionnalités de la méthode afin que chaque méthode traite des données logiquement liées.

Groupes de données

Des groupes de données logiquement liées 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é d'une liste de champs, ces champs doivent être déplacés vers une classe distincte (classe d'extraction).

Solutions qui violent les principes de la POO

Ces "odeurs" se produisent lorsqu'un développeur viole la conception appropriée de la POO. Cela se produit lorsqu'il ou elle ne comprend pas entièrement les capacités de la POO et ne les utilise pas pleinement ou correctement.

Ne pas utiliser l'héritage

Si une sous-classe n'utilise qu'un petit sous-ensemble des fonctions de la classe parente, cela sent la mauvaise hiérarchie. Lorsque cela se produit, les méthodes superflues ne sont généralement pas remplacées ou génèrent des exceptions. Une classe héritant d'une autre implique que la classe enfant utilise presque toutes les fonctionnalités de la classe parent. Exemple de hiérarchie correcte : Comment fonctionne la refactorisation en Java - 2Exemple de hiérarchie incorrecte : Comment fonctionne la refactorisation en Java - 3

Instruction de commutation

Quel pourrait être le problème avec une switchdéclaration ? C'est mauvais quand ça devient très complexe. Un problème connexe est un grand nombre d' ifinstructions imbriquées.

Classes alternatives avec différentes interfaces

Plusieurs classes font la même chose, mais leurs méthodes ont des noms différents.

Champ temporaire

Si une classe a un champ temporaire dont un objet n'a besoin qu'occasionnellement lorsque sa valeur est définie, et qu'il est vide ou, à Dieu ne plaise, nullle reste du temps, alors le code sent mauvais. C'est une décision de conception discutable.

Odeurs qui rendent la modification difficile

Ces odeurs sont plus graves. D'autres odeurs compliquent principalement la compréhension du code, mais elles vous empêchent de le modifier. Lorsque vous essayez d'introduire de nouvelles fonctionnalités, la moitié des développeurs abandonnent et l'autre moitié devient folle.

Hiérarchies d'héritage parallèles

Ce problème se manifeste lorsque la sous-classe d'une classe vous oblige à créer une autre sous-classe pour une classe différente.

Dépendances uniformément réparties

Toute modification nécessite que vous recherchiez toutes les utilisations d'une classe (dépendances) et que vous fassiez beaucoup de petits changements. Un changement - modifications dans de nombreuses classes.

Arbre complexe de modifications

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

"Les ordures sentent bon"

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

Un grand nombre de commentaires dans une méthode

Une méthode a beaucoup de commentaires explicatifs sur presque chaque ligne. Cela est généralement dû à 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 explicatifs.

Code dupliqué

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

Classe paresseux

Une classe prend très peu de fonctionnalités, bien qu'elle ait été prévue pour être grande.

Code inutilisé

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

Connectivité excessive

Cette catégorie d'odeurs se caractérise par un grand nombre de relations injustifiées dans le code.

Méthodes externes

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

Intimité inappropriée

Une classe dépend des détails d'implémentation d'une autre classe.

Appels de longue durée

Une classe en appelle une autre, qui demande des données à une troisième, qui obtient des données à une quatrième, et ainsi de suite. Une si longue chaîne d'appels signifie une forte dépendance à l'égard de la structure de classe actuelle.

Classe de tâche-revendeur

Une classe n'est nécessaire que pour envoyer une tâche à une autre classe. Peut-être faudrait-il l'enlever ?

Techniques de refactorisation

Ci-dessous, nous discuterons des techniques de refactorisation de base qui peuvent aider à éliminer les odeurs de code décrites.

Extraire une classe

Une classe exécute trop de fonctions. Certains d'entre eux doivent être déplacés vers une autre classe. Par exemple, supposons que nous ayons une Humanclasse qui stocke également une adresse personnelle et a une méthode qui renvoie 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();
    }
 }
Il est recommandé de placer les informations d'adresse et la méthode associée (comportement de 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();
    }
 }

Extraire une méthode

Si une méthode a des fonctionnalités qui peuvent être isolées, vous devez la placer 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");
        }
    }
Nous calculons chacune 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 facile à comprendre.

Passer un objet entier

Lorsqu'une méthode est appelée avec des paramètres, vous pouvez parfois voir un code comme celui-ci :

 public void employeeMethod(Employee employee) {
     // Some actions
     double yearlySalary = employee.getYearlySalary();
     double awards = employee.getAwards();
     double monthlySalary = getMonthlySalary(yearlySalary, awards);
     // Continue processing
 }
 
 public double getMonthlySalary(double yearlySalary, double awards) {
      return (yearlySalary + awards)/12;
 }
Le employeeMethoda 2 lignes entières consacrées à la réception des valeurs et à leur stockage dans des variables primitives. Parfois, ces constructions peuvent prendre jusqu'à 10 lignes. Il est beaucoup plus facile de passer l'objet lui-même et de l'utiliser pour extraire les données nécessaires :

 public void employeeMethod(Employee employee) {
     // Some actions
     double monthlySalary = getMonthlySalary(employee);
     // Continue processing
 }
 
 public double getMonthlySalary(Employee employee) {
     return (employee.getYearlySalary() + employee.getAwards())/12;
 }

Simple, bref et concis.

En regroupant logiquement les champs et en les déplaçant séparément, classDespitele fait que les exemples ci-dessus sont très simples, et quand vous les regardez, beaucoup d'entre vous peuvent demander, "Qui fait ça ?", de nombreux développeurs font de telles erreurs structurelles à cause de la négligence, réticence à refactoriser le code, ou simplement une attitude de "c'est assez bien".

Pourquoi le refactoring est efficace

Grâce à une bonne refactorisation, un programme a un code facile à lire, la perspective de modifier sa logique n'est pas effrayante, et l'introduction de nouvelles fonctionnalités ne devient pas un enfer d'analyse de code, mais est plutôt une expérience agréable pendant quelques jours . Vous ne devriez pas refactoriser s'il serait plus facile d'écrire un programme à partir de zéro. Par exemple, supposons que votre équipe estime que le travail nécessaire pour comprendre, analyser et refactoriser le code sera supérieur à la mise en œuvre de la même fonctionnalité à partir de zéro. Ou si le code à refactoriser présente de nombreux problèmes difficiles à déboguer. Savoir améliorer la structure du code est essentiel dans le travail d'un programmeur. Et apprendre à programmer en Java se fait mieux sur CodeGym, le cours en ligne qui met l'accent sur la pratique. Plus de 1200 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 c'est maintenant :)

Ressources pour vous immerger davantage dans le refactoring

Le livre le plus célèbre sur le refactoring est "Refactoring. Improving the Design of Existing Code" de Martin Fowler. Il existe également une publication intéressante sur le refactoring, basée sur un livre précédent : "Refactoring Using Patterns" de Joshua Kerievsky. En parlant de patrons... Lors du refactoring, il est toujours très utile de connaître les patrons de conception de base. Ces excellents livres vous y aideront : En parlant de patrons... Lors de la refactorisation, il est toujours très utile de connaître les patrons de conception de base. Ces excellents livres vous y aideront :
  1. "Design Patterns" par Eric Freeman, Elizabeth Robson, Kathy Sierra et Bert Bates, de la série Head First
  2. "L'art du code lisible" par Dustin Boswell et Trevor Foucher
  3. "Code Complete" de Steve McConnell, qui énonce les principes d'un code beau et élégant.