El Arte del Código Limpio o «Clean Code» en el Entorno Java

Blog

El libro “Código Limpio” escrito por Robert C. Martin, también conocido como “Tío Bob”, es una referencia esencial sobre las buenas prácticas de código limpio. En este artículo resumiremos algunas de estas mejores prácticas con algunos ejemplos en el entorno Java.

1. De qué se trata el Código Limpio

Concepto de Código Limpio
Concepto de Código Limpio

Antes de entrar en los detalles del “código limpio”, vamos a explicar lo que significa. El término “código limpio” puede resumirse como un conjunto de prácticas que deben aplicarse para que el código sea más fácil de mantener y entender por otros desarrolladores. Estas prácticas pueden encontrarse en todos los niveles de desarrollo de software, algunas son muy sencillas de aplicar y otras requieren un poco más de experiencia.

Siempre que oímos hablar de código limpio, nos encontramos con una referencia a Martin Fowler, autor de “Refactoring: Improving the Design of Existing Code“. Así es como describe el código limpio en su obra:

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

Lo que se traduce en: Cualquier tonto puede escribir un código que un ordenador pueda entender. Los buenos programadores escriben código que los humanos pueden entender.

2. ¿Por qué Escribir Código Limpio?

Escribir código limpio es una cuestión de habilidad, pero sobre todo es un hábito que hay que desarrollar. Cuanta más experiencia adquiramos, mejor será la calidad de nuestros productos. Pero, ¿por qué invertir tiempo en aprender y aplicar un código limpio?

  • El coste real de una aplicación es el mantenimiento (más del 60% del coste de la aplicación según un estudio de la consultora Accenture y publicado en “How Software Maintenance Fees Are Siphoning Away Your IT Budget – and How to Stop It“)
  • Pasamos 10 veces más tiempo leyendo código que escribiéndolo (Robert C. Martin en su libro: “Código Limpio: Manual de estilo para el desarrollo ágil de software“)
  • Para adaptarse más fácilmente al cambio.
  • Para tener menos errores debidos a malentendidos del código.
  • Una solución más comprobable, como veremos más adelante en el artículo, el código está más descompuesto y es más comprobable.
  • Y, por último, una fuente de motivación para que los desarrolladores trabajen en una aplicación bien pensada y codificada.

3. Indicadores de Código Defectuoso o “Codes Smells”

El término « codes smells » fue acuñado por Martin Fowler y Kent Beck, y existen diferentes opiniones entre los desarrolladores, no sobre los indicadores de código malo, sino sobre el límite en el que se considera malo. Por ejemplo, sobre el tamaño de una clase o un método, o el número de parámetros de un método, algunos considerarán que este límite de hasta seis parámetros sigue siendo apropiado, otros serán más estrictos y establecerán este límite en dos o tres parámetros.

Aquí no se busca la estética, sino construir una aplicación mucho más robusta y mantenible y así evitar en lo posible los bugs.

Entre estos indicadores encontramos :

  • Código duplicado ;
  • Método demasiado largo ;
  • Método con varias intenciones;
  • Nombres no hablados ;
  • Números mágicos;
  • No se utilizan las constantes ;
  • Clase demasiado larga ;
  • Método con demasiados parámetros de entrada ;

4. Los Principios y Reglas Básicas del Código Limpio

4.1. DRY: Don’t Repeat Yourself (No te repitas)

La duplicación de código es una de las lacras del desarrollo de software y causa muchos problemas en su mantenimiento. Esta duplicación de código es la causa de muchos errores difíciles de identificar y también dificulta la aplicación de los cambios.

Es bastante sencillo aplicar este principio, a diferencia de otros patrones de diseño avanzados, tienes que romper tu sistema en piezas, dividir el código y la lógica en las unidades más pequeñas posibles, métodos concisos con responsabilidades identificadas.

Un método largo corre el riesgo de incrustar lógica que puede ser exportada a otros casos de uso, y el mantenimiento de esta lógica en otros casos de uso será mucho más complicado.

4.2. KISS : Keep It Simple Stupid (Mantenerlo tan simple como sea posible)

Este principio es sencillo, hay que ir a lo esencial, no desarrollar funcionalidades superfluas si no hay necesidad en el momento, si esta necesidad existe algún día, seguramente habrá que adaptarlo para satisfacerla, lo que supondrá un mayor coste.

La necesidad también debe satisfacerse de la manera más lógica y sencilla posible, no mediante abstracciones demasiado complicadas que usted mismo no entenderá unos meses después.

También se identificará el coste de mantenimiento del software, ya que será más fácil que otros desarrolladores se ocupen de tu código.

4.3. YAGNI : You Ain’t Gonna Neet It (No lo necesitarás)

(No lo necesitarás)

Este principio es similar al anterior sobre la utilidad de una característica, no se debe desarrollar una característica si no hay una necesidad funcional real.

A muchos desarrolladores les gusta producir funciones “extra”, pero en la mayoría de los casos estas funciones nunca se utilizarán y, si lo hacen, habrá que adaptarlas.

Las desventajas de esta práctica pueden parecer obvias, pero a continuación se enumeran:

  • Tiempo dedicado a la funcionalidad no planificada en detrimento de la funcionalidad planificada en la hoja de ruta
  • Tiempo de prueba/revisión por parte del equipo, lanzamiento/producción, etc.
  • Tiempo de mantenimiento
  • Puede ser una práctica común del equipo más adelante

Según la “Programación Extrema”, « Extreme Programming » (XP), un método ágil orientado al aspecto de la realización de una aplicación, es incluso preferible basarse en la “refactorización” del código existente que en la planificación de una futura implementación de características.

4.4. SOLID: 5 letras para 5 principios

S : Single Responsability (Cada clase/función… debe tener una sola responsabilidad) (Tío Bob)

¿Número de líneas para una función? ¿Una clase?

  • Función: de 5 a 10 líneas como máximo
  • Clase: Tamaño de la pantalla (evitar el desplazamiento)

O : Open/Closed principle

Cada software debe estar abierto a la ampliación y cerrado a la modificación, por ejemplo aquí tenemos un cliente que utiliza una impresora, la impresora utilizada es Printer1, por lo que el cliente conoce la implementación de esta impresora, si esta implementación cambia tendrá que modificar su llamada a esta impresora para adaptarla:

Principio Open Closed de código limpio
Principio Open Closed de código limpio

Aquí una forma sencilla de corregir este problema de diseño es utilizar una abstracción que todas las impresoras implementen, el cliente entonces sólo llamará a las funciones que se describen en la abstracción “Printer”.

Aquí nuestro software está cerrado a la modificación, es decir, la abstracción no se modificará, pero por otro lado está abierto a la extensión, podremos añadir tantas implementaciones de impresoras como queramos:

Principio Open Closed de código limpio
Principio Open Closed de código limpio

L : Liskov substitution principle (LSP)

Si estás trabajando con una clase abstracta, el uso de esta o aquella implementación no debe cambiar el contrato.

Matemáticamente, un cuadrado es un rectángulo (al menos, un caso especial). Sin embargo, en nuestro caso, esto no es cierto.

En efecto, la clase Cuadrado tiene un invariante particular, a saber, que en cualquier momento t, su altura y su anchura deben ser iguales (en sí, la noción de altura o anchura carece de sentido semántico en el caso de un cuadrado).

Ahora bien, si inicializar un rectángulo con una anchura de 5 y una altura de 2 (por ejemplo) tiene sentido, se vuelve aberrante cuando hablamos de un cuadrado. De hecho, el comportamiento de una clase Square es contrario al de nuestra clase Rectangle. En ese caso, no se respeta el LSP.

Principio de sustitución de Liskov
Principio de sustitución de Liskov
@Test
void should_return_6_when_getting_area_with_3_height_and_2_width() {
    calculateArea(new LiskovSubstitutionPrinciple.Rectangle());
    calculateArea(new LiskovSubstitutionPrinciple.Square());
}
void calculateArea(LiskovSubstitutionPrinciple.Rectangle rectangle) {
    // given
    rectangle.setHeight(3);
    rectangle.setWidth(2);
    // when
    double area = rectangle.getArea();
    // then
    assertEquals(area, 6);
}

I : Interface segregation principle

Un cliente nunca debe ser obligado a depender de una interfaz que no utiliza (Robert C. Martin).

Imaginemos esta interfaz “IWorker”, que declara dos firmas de métodos, work y eat:

public interface IWorker {
    void work();
    void eat();
}

Aquí hay dos posibles implementaciones “Worker” y “GreatWorker“, aquí los empleados trabajarán durante sus horas de trabajo y comerán durante sus descansos, no hay problema hasta ahora. Nuestros dos métodos están bien alimentados y son diferentes entre las dos implementaciones.

public class Worker implements IWorker {
    @Override
    public void work() {
        // working
    }
    @Override
    public void eat() {
        // eating in break
    }
}
public class GreatWorker implements IWorker {
    @Override
    public void work() {
        // working much more
    }
    @Override
    public void eat() {
        // eating in break
    }
}

Por otro lado, imaginemos esta tercera implementación, la de “RobotWorker“, aquí ya no es un empleado sino un robot que va a trabajar, el robot no necesita comer, por lo que se ha ignorado el método “eat“.

Así que aquí se ha violado el principio y dependemos de una interfaz que no utilizamos completamente.

public class RobotWorker implements IWorker {
    @Override
    public void work() {
        // working much more
    }
    @Override
    public void eat() {
    }
}

D : Dependency inversion principle

  • Los módulos de alto nivel no deben depender de los de bajo nivel. Ambos deben depender de las abstracciones.
  • Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.

Ejemplo aquí con la inversión de la dependencia vamos a través de una interfaz que no evolucionará dependiendo de la implementación en lugar de llamar directamente a la capa de acceso de datos a través de la implementación específica:

Principio de inversión de dependencia
Principio de inversión de dependencia

5. Algunas Reglas para Aplicar en el Día a Día de un Desarrollador

5.1. Nomenclatura

La nomenclatura es uno de los elementos más importantes de nuestro código, el código que escribimos será leído y mantenido por otros humanos.

Al leer una variable, un desarrollador debe ser capaz de entender su propósito, al leer una función, debe saber lo que hará:

  • Los nombres deben revelar la intención
  • Las clases deben tener nombres, también hay que evitar la información redundante en sus nombres
    • UserInfo : aquí el término Info es inútil, en la clase User tendremos atributos que corresponden a la información del usuario (nombre/apellido, etc…)
  • Las funciones deben ser verbos: getXXX(), setXXX(), createXXX(), validateXXX()
  • Los booleanos deben ser Sí/No: isXXX(), areXXXX()
  • Utiliza palabras que se puedan buscar fácilmente en el código y que tengan sentido
  • Los nombres deben ser pronunciables
    • No abusar de las abreviaturas (salvo para conceptos empresariales básicos…), por ejemplo getInvsttBnkRt
  • DRY:
    • User.getUserName, User.getUserCity: aquí se repite el término User aunque estemos en la clase user, lo que va en contra del principio “DRY: don’t repeat yourself“.
  • No dudes en cambiar un nombre, de hecho los IDEs te permiten cambiar los nombres muy fácilmente, si encuentras un nombre que corresponde mejor a la intención deseada.
  • Evita los números mágicos, un número mágico es un número que se usa sin que su nombre revele su propósito.

Ejemplo de número mágico:

public class Foo {
    public void setPassword(String password) {
         // don't do this
         if (password.length() > 12) {
              throw new InvalidArgumentException("password");
         }
    }
}

Ahora, ocultaremos la longitud de caracteres en una variable.

public class Foo {
    public static final int MAX_PASSWORD_SIZE = 12;
    public void setPassword(String password) {
         if (password.length() > MAX_PASSWORD_SIZE) {
              throw new InvalidArgumentException("password");
         }
    }
}

5.2. Evitar los efectos secundarios (Side Effects)

Los efectos secundarios son malos y a menudo esconden errores, y son una práctica común en el código. A menudo ocurre que si pasamos un objeto como parámetro a una función y lo actualizamos por referencia, nos arriesgamos a añadir aún más responsabilidad a esta función.

La función nos miente porque no indica en su nombre lo que realmente hace. Esto es una fuente de errores y comportamientos no deseados, especialmente cuando estas actualizaciones están enterradas en un gran volumen de código.

5.3. Principio de devolución anticipada

Según este principio, las funciones deben escribirse de tal manera que el retorno positivo esperado de una función se devuelva al final de la misma y que todo el resto del código termine la ejecución cuando no se cumplan las condiciones.

Tomemos el ejemplo del código siguiente, tenemos aquí una anidación de verificaciones una dentro de otra, esto complica enormemente la lectura y la comprensión de este código. El tratamiento que nos interesa en esta función se esconde en el centro de la misma, donde se posiciona el comentario: // Action if allowed

public int confusingFonction(String name, int value, AuthenticationInfo permissions) {
    int retval = SUCCESS;
    if (globalCondition) {
        if (name != null && !name.equals("")) {
            if (value != 0) {
                if (permissions.allow(name)) {
                    // Action if allowed
                } else {
                    retval = DENY;
                }
            } else {
                retval = BAD_VALUE;
            }
        } else {
            retval = INVALID_NAME;
        }
    } else {
        retval = BAD_COND;
    }
    return retval;
}

Intentemos mejorar este código aplicando el principio de retorno rápido, necesitamos tener todas las comprobaciones, excepciones, parámetros malos comprobados antes de procesar nuestra función:

public int lessConfusingFonction(String name, int value, AuthenticationInfo perms) {
    if (!globalCondition) {
        return BAD_COND;
    }
    if (name == null || name.equals("")) {
        return BAD_NAME;
    }
    if (value == 0) {
        return BAD_VALUE;
    }
    if (!perms.allow(name)) {
        return DENY;
    }
    // Action if allowed
    return SUCCESS;
}

5.4. Evitar return null

Tony Hoare, que inventó el null en 1965, se arrepiente de su invento: “Lo llamo mi error de mil millones de dólares“.

Su objetivo era garantizar que cualquier uso de las referencias fuera absolutamente seguro, y que el compilador lo comprobara automáticamente. Pero no pudo resistir la tentación de poner una referencia nula, simplemente porque era fácil de implementar.

Esto ha dado lugar a innumerables errores, vulnerabilidades y fallos del sistema, que probablemente han causado un billón de dólares de sufrimiento y daños en los últimos cuarenta años.

Existen varias soluciones para aplicar este principio, para un método que devuelve una colección, en lugar de devolver un null, es posible sustituirlo por una colección vacía. Desde Java 5 tenemos el método “Collections.emptyList()” por ejemplo o en Java 9 con “List.of()“, estos dos métodos devuelven listas inmutables, por lo que el llamador no las modificará.

private List<Integer> getMovieYears2(List<Movie> movies) {
    if (movies == null) {
        return Collections.emptyList();
    }
    List<Integer> years = new ArrayList<>();
    for (Movie movie : movies) {
        years.add(movie.getYear());
    }
    return years;
}

También hay otras soluciones, por ejemplo en Java 8 tenemos la nueva clase “Optional” que se ha añadido. Esta clase es un tipo de contenedor que está vacío o contiene el valor no nulo.

Brian Goetz, arquitecto de Java en Oracle, explica las razones para añadir esta nueva clase:

“Nuestra intención era proporcionar un mecanismo limitado para los tipos de retorno de los métodos de la biblioteca en los que era necesario que hubiera una forma clara de representar “ningún resultado”, y el uso de null para ello era abrumadoramente probable que causara errores.”

Como apunte, hay que señalar que el objetivo es utilizar la clase Optional sólo en los retornos de las funciones. Si necesitamos un Opcional en los parámetros de una función, significa que la función tendrá dos funciones, una si el Opcional está presente, una segunda si no está presente, por lo que debemos crear dos funciones y condicionar la llamada según el Opcional. Tampoco debes utilizarlo como atributo de una clase (Optional no es serializable).

Todavía podemos mejorar esta función utilizando api que faciliten la lectura (aquí la api “stream” y el desglose en pipelines facilita la lectura, una acción = una línea).

private List<Integer> getMovieYears3(List<Movie> movies) {
    if (movies == null) {
        return Collections.emptyList();
    }
    return movies.stream()
                 .map(Movie::getYear)
                 .collect(Collectors.toList());
}

O simplificando la comprobación de null permitiendo el uso de una colección potencialmente null en el stream:

private List<Integer> getMovieYears5(List<Movie> movies) {
    return Stream.ofNullable(movies)
                 .flatMap(Collection::stream)
                 .map(Movie::getYear)
                 .collect(Collectors.toList());
}

5.5. Comentarios

No comentes el código incomprensible, hazlo comprensible

Tío Bob: “Los comentarios son siempre un fracaso”,

  • Mienten, envejecen mal, no son refactorizables
  • Demuestra el fracaso de:
    • Utilizar un buen nombre
    • Crear una abstracción.
    • Dividir en métodos de una sola intención
  • Excepto para las explicaciones (Javadoc, algoritmos matemáticos, soluciones de errores, legales, derechos de autor…)

6. Consejos

  • Simplificar los métodos largos haciendo extracciones de métodos de intención única
    • También mejora el rendimiento, métodos HOT para JIT (Just in time compiler)
  • Corregir el código duplicado mediante el uso de clases de utilidad o el uso de la herencia
  • Evitar pasar demasiados parámetros a un método (2 o 3 parámetros como máximo), de lo contrario, pensar en convertirlo en un objeto o rediseñarlo.
    • Si la función hace demasiadas cosas (principio de responsabilidad única), entonces redúcelo.
    • Evitar las combinaciones de booleanos en las funciones
  • No hay parámetros nulos, si tenemos dos casos de uso para el método debemos crear dos métodos diferentes con la intención claramente identificada en el nombre.

7. Conclusión

Entiendo que es difícil redactar buenos programas, a veces debido al calendario o a los diferentes hitos de tu proyecto. Pero, ¿cuánto tiempo lo retrasarás?

Tu código puede hacer maravillas por ti y sobre todo por los demás. Como desarrolladores, estamos mejorando constantemente. Si examinas un trozo de código que desarrollaste tú mismo hace unos años, seguro que encuentras áreas de mejora.

Seguir estas sencillas reglas de código limpio no es difícil y te ahorrará muchos dolores de cabeza en el futuro, estarás creando un sistema intrínsecamente comprobable, con todos los beneficios que ello implica.

Cuando una de las partes externas del sistema se queda obsoleta, como la base de datos o el framework de la web, puedes sustituir estos elementos obsoletos con un mínimo de complicaciones.

Sobre el Autor:

Hey hola! Yo soy Alex Walton y tengo el placer de compartir conocimientos hacía ti sobre el tema de Programación en Java, desde cero, Online y Gratis.

Deja una Respuesta

*

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.