Cuando se aprende a programar, se dedica mucho tiempo a escribir código. La mayoría de los desarrolladores principiantes creen que esta es su actividad futura. Esto es parcialmente cierto, pero las tareas de un programador también incluyen mantener y refactorizar el código. Hoy hablaremos sobre refactorización. Cómo funciona la refactorización en Java - 1

Refactorización en el curso JavaRush

El curso JavaRush cubre el tema de la refactorización dos veces: Gracias a una gran tarea, existe la oportunidad de familiarizarse con la refactorización real en la práctica, y una conferencia sobre refactorización en IDEA le ayudará a comprender las herramientas automáticas que hacen la vida increíblemente más fácil.

¿Qué es la refactorización?

Este es un cambio en la estructura del código sin cambiar su funcionalidad. Por ejemplo, existe un método que compara 2 números y devuelve verdadero si el primero es mayor y falso en caso contrario:
public boolean max(int a, int b) {
    if(a > b) {
        return true;
    } else if(a == b) {
        return false;
    } else {
        return false;
    }
}
El resultado fue un código muy engorroso. Incluso los principiantes rara vez escriben algo como esto, pero existe un gran riesgo. Parecería, ¿por qué hay un bloque aquí if-elsesi puedes escribir un método 6 líneas más corto?
public boolean max(int a, int b) {
     return a>b;
}
Ahora bien, este método parece simple y elegante, aunque hace lo mismo que el ejemplo anterior. Así funciona la refactorización: cambia la estructura del código sin afectar su esencia. Existen muchos métodos y técnicas de refactorización, que consideraremos con más detalle.

¿Por qué es necesaria la refactorización?

Hay varias razones. Por ejemplo, la búsqueda de la simplicidad y concisión del código. Los defensores de esta teoría creen que el código debe ser lo más conciso posible, incluso si se requieren docenas de líneas de comentarios para entenderlo. Otros desarrolladores creen que el código debería refactorizarse para que sea comprensible con un número mínimo de comentarios. Cada equipo elige su propia posición, pero debemos recordar que refactorizar no es una reducción . Su principal objetivo es mejorar la estructura del código. En esta meta global se pueden incluir varios objetivos:
  1. La refactorización mejora la comprensión del código escrito por otro desarrollador;
  2. Ayuda a encontrar y corregir errores;
  3. Le permite aumentar la velocidad de desarrollo de software;
  4. En general, mejora la composición del software.
Si la refactorización no se lleva a cabo durante un tiempo prolongado, pueden surgir dificultades en el desarrollo, hasta la parada total del trabajo.

“El código huele”

Cuando el código requiere refactorización, dicen que "huele". Por supuesto, no literalmente, pero ese código realmente no se ve muy bien. A continuación consideraremos las principales técnicas de refactorización para la etapa inicial.

Elementos innecesariamente grandes

Hay clases y métodos engorrosos con los que es imposible trabajar de forma eficaz precisamente debido a su enorme tamaño.

gran clase

Una clase de este tipo tiene una gran cantidad de líneas de código y muchos métodos diferentes. Por lo general, es más fácil para un desarrollador agregar una característica a una clase existente en lugar de crear una nueva, razón por la cual crece. Como regla general, la funcionalidad de esta clase está sobrecargada. En este caso, es útil separar parte de la funcionalidad en una clase separada. Hablaremos de esto con más detalle en la sección de técnicas de refactorización.

Gran método

Este "olor" ocurre cuando un desarrollador agrega una nueva funcionalidad a un método. “¿Por qué debería poner la verificación de parámetros en un método separado si puedo escribirlo aquí?”, “¿Por qué es necesario separar el método para encontrar el elemento máximo en la matriz? Dejémoslo aquí. De esta manera el código es más claro”, y otros conceptos erróneos. Hay dos reglas para refactorizar un método grande:
  1. Si, al escribir un método, desea agregar un comentario al código, debe separar esta funcionalidad en un método separado;
  2. Si un método requiere más de 10 a 15 líneas de código, debe identificar las tareas y subtareas que realiza e intentar separar las subtareas en un método separado.
Varias formas de eliminar un método grande:
  • Separar parte de la funcionalidad de un método en un método separado;
  • Si las variables locales no le permiten extraer parte de la funcionalidad, puede pasar el objeto completo a otro método.

Usando muchos tipos de datos primitivos

Normalmente, este problema ocurre cuando la cantidad de campos para almacenar datos en una clase crece con el tiempo. Por ejemplo, si utiliza tipos primitivos en lugar de objetos pequeños para almacenar datos (moneda, fecha, números de teléfono, etc.) o constantes para codificar cualquier información. Una buena práctica en este caso sería agrupar lógicamente los campos y colocarlos en una clase separada (seleccionando una clase). También puede incluir métodos para procesar estos datos en la clase.

Larga lista de opciones

Un error bastante común, especialmente en combinación con un método amplio. Suele ocurrir si la funcionalidad del método está sobrecargada o si el método combina varios algoritmos. Las listas largas de parámetros son muy difíciles de entender y estos métodos son incómodos de utilizar. Por tanto, es mejor transferir todo el objeto. Si el objeto no tiene suficientes datos, vale la pena usar un objeto más general o dividir la funcionalidad del método para que procese datos relacionados lógicamente.

Grupos de datos

En el código suelen aparecer grupos de datos lógicamente relacionados. Por ejemplo, parámetros de conexión a la base de datos (URL, nombre de usuario, contraseña, nombre de esquema, etc.). Si no se puede eliminar ni un solo campo de la lista de elementos, entonces la lista es un grupo de datos que debe colocarse en una clase separada (selección de clase).

Soluciones que estropean el concepto de programación orientada a objetos

Este tipo de "olor" ocurre cuando el desarrollador viola el diseño de programación orientada a objetos. Esto sucede si no comprende completamente las capacidades de este paradigma, las usa de manera incompleta o incorrecta.

Rechazo de herencia

Si una subclase utiliza una parte mínima de las funciones de la clase principal, huele a jerarquía incorrecta. Normalmente, en este caso, los métodos innecesarios simplemente no se anulan ni se generan excepciones. Si una clase se hereda de otra, esto implica un uso casi completo de su funcionalidad. Ejemplo de jerarquía correcta: Cómo funciona la refactorización en Java - 2 Ejemplo de jerarquía incorrecta: Cómo funciona la refactorización en Java - 3

declaración de cambio

¿Qué podría estar mal con un operador switch? Es malo cuando su diseño es muy complejo. Esto también incluye muchos bloques anidados if.

Clases alternativas con diferentes interfaces.

En realidad, varias clases hacen lo mismo, pero sus métodos tienen nombres diferentes.

Campo temporal

Si la clase contiene un campo temporal que el objeto sólo necesita ocasionalmente, cuando está lleno de valores, y el resto del tiempo está vacío o, Dios no lo quiera, nullentonces el código "huele", y tal diseño es dudoso. decisión.

Olores que dificultan la modificación

Estos "olores" son más graves. El resto perjudica principalmente la comprensión del código, mientras que no permiten modificarlo. Al introducir alguna característica, la mitad de los desarrolladores renunciarán y la otra mitad se volverá loca.

Jerarquías de herencia paralelas

Cuando crea una subclase de una clase, debe crear otra subclase para otra clase.

Distribución uniforme de dependencia

Al realizar cualquier modificación, hay que buscar todas las dependencias (usos) de esta clase y realizar muchos pequeños cambios. Un cambio: ediciones en muchas clases.

Árbol de modificación complejo

Este olor es opuesto al anterior: los cambios afectan a una gran cantidad de métodos de la misma clase. Como regla general, la dependencia en dicho código se produce en cascada: después de cambiar un método, es necesario arreglar algo en otro, luego en un tercero, y así sucesivamente. Una clase, muchos cambios.

“Huele a basura”

Una categoría de olores bastante desagradable que provoca dolores de cabeza. Código antiguo, inútil e innecesario. Afortunadamente, los IDE y linters modernos han aprendido a advertir sobre esos olores.

Una gran cantidad de comentarios en el método.

El método tiene muchos comentarios explicativos en casi cada línea. Esto suele estar asociado con un algoritmo complejo, por lo que es mejor dividir el código en varios métodos más pequeños y darles nombres significativos.

Duplicación de código

Diferentes clases o métodos utilizan los mismos bloques de código.

clase perezosa

La clase adquiere muy poca funcionalidad, aunque gran parte de ella estaba planificada.

Código no utilizado

Una clase, método o variable no se utiliza en el código y es "peso muerto".

Acoplamiento excesivo

Esta categoría de olores se caracteriza por una gran cantidad de conexiones innecesarias en el código.

Métodos de terceros

Un método utiliza los datos de otro objeto con mucha más frecuencia que sus propios datos.

Intimidad inapropiada

Una clase utiliza campos de servicio y métodos de otra clase.

llamadas de clase largas

Una clase llama a otra, que solicita datos de la tercera, de la cuarta, y así sucesivamente. Una cadena de llamadas tan larga significa un alto nivel de dependencia de la estructura de clases actual.

Distribuidor de tareas de clase

Sólo se necesita una clase para pasar una tarea a otra clase. ¿Quizás debería eliminarse?

Técnicas de refactorización

A continuación hablaremos sobre las técnicas de refactorización inicial que ayudarán a eliminar los olores de código descritos.

selección de clase

La clase realiza demasiadas funciones; algunas de ellas deben trasladarse a otra clase. Por ejemplo, hay una clase Humanque también contiene una dirección residencial y un método que proporciona la dirección completa:
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();
   }
}
Sería una buena idea colocar la información de la dirección y el método (comportamiento de procesamiento de datos) en una clase separada:
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();
   }
}

Selección de método

Si alguna funcionalidad de un método se puede agrupar, se debe colocar en un método separado. Por ejemplo, un método que calcula las raíces de una ecuación cuadrática:
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");
    }
}
Traslademos el cálculo de las tres opciones posibles a métodos separados:
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");
}
El código para cada método se ha vuelto mucho más corto y claro.

Transferir todo el objeto

Al llamar a un método con parámetros, a veces puedes ver un código como este:
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;
}
En el método, employeeMethodse asignan hasta 2 líneas para obtener valores y almacenarlos en variables primitivas. A veces, estos diseños ocupan hasta 10 líneas. Es mucho más fácil pasar el objeto en sí al método, desde donde se pueden extraer los datos necesarios:
public void employeeMethod(Employee employee) {
    // Некоторые действия
    double monthlySalary = getMonthlySalary(employee);
    // Продолжение обработки
}

public double getMonthlySalary(Employee employee) {
    return (employee.getYearlySalary() + employee.getAwards())/12;
}
Sencillo, breve y conciso.

Agrupación lógica de campos y colocación en una clase separada.

A pesar de que los ejemplos anteriores son muy simples y, al mirarlos, muchos pueden preguntarse "¿Quién hace esto realmente?", muchos desarrolladores, debido a la falta de atención, la falta de voluntad para refactorizar el código o simplemente "Esto servirá", hacen errores estructurales similares.

Por qué la refactorización es efectiva

El resultado de una buena refactorización es un programa cuyo código es fácil de leer, las modificaciones en la lógica del programa no se convierten en una amenaza y la introducción de nuevas funciones no se convierte en un infierno de análisis de código, sino en una actividad agradable durante un par de días. . No se debe utilizar la refactorización si fuera más fácil reescribir el programa desde cero. Por ejemplo, el equipo estima que los costos laborales para analizar, analizar y refactorizar el código son mayores que los de implementar la misma funcionalidad desde cero. O el código que necesita ser refactorizado tiene muchos errores que son difíciles de depurar. Saber mejorar la estructura del código es obligatorio en el trabajo de un programador. Bueno, es mejor aprender programación Java en JavaRush, un curso en línea con énfasis en la práctica. Más de 1200 tareas con verificación instantánea, alrededor de 20 miniproyectos, tareas de juegos: todo esto te ayudará a sentirte seguro al codificar. El mejor momento para empezar es ahora :) Cómo funciona la refactorización en Java - 4

Recursos para profundizar más en la refactorización

El libro más famoso sobre refactorización es “Refactoring. Mejorando el diseño del código existente” por Martin Fowler. También hay una publicación interesante sobre refactorización, escrita en base a un libro anterior: "Refactoring with Patterns" de Joshua Kiriewski. Hablando de plantillas. Al refactorizar, siempre es muy útil conocer los patrones básicos de diseño de aplicaciones. Estos fantásticos libros le ayudarán con esto:
  1. “Patrones de diseño” - de Eric Freeman, Elizabeth Freeman, Kathy Sierra, Bert Bates de la serie Head First;
  2. “Código legible o programación como arte” - Dustin Boswell, Trevor Faucher.
  3. “Perfect Code” de Steve McConnell, que describe los principios de un código hermoso y elegante.
Bueno, algunos artículos sobre refactorización:
  1. Una tarea increíble: comencemos a refactorizar el código heredado ;
  2. Refactorización ;
  3. Refactorización para todos .