Buenas prácticas de programación sencillas en el código fuente

Escrito por el .
java planeta-codigo programacion
Enlace permanente Comentarios

Al escribir código uno de las principales objetivos además de que funcione es tan importante que sea código legible ya que la mayor parte del tiempo en la programación generalmente no se emplea a escribir código nuevo sino a mantener código existente. Las prácticas de este artículo para escribir código más legible son sencillas de comprender y de aplicar en cualquier lenguaje de programación.

Tan importante que un programa funcione y esté libre de errores es que el código fuente del programa sea legible y fácil de entender. Esto permite que en caso de un error el análisis del código fuente sea más sencillo, incluso si es código escrito por uno mismo, cuando pasan varios meses e incluso años desde que se escribió el código uno ya no se acuerda de ninguno de sus detalles y hay que analizarlo como si lo hubiese escrito otra persona.

La legibilidad afecta a la facilidad de cambio del código fuente y a la facilidad de corrección de errores y tiempo que se tarda en resolverlos. En realidad la mayor parte del tiempo dedicado a la programación no es a la escritura de nuevas líneas de código fuente si no a la lectura de las existentes, por ello al escribir código este debería ser optimizado para la lectura para la mayoría de ámbitos y aplicaciones más que a la eficiencia. Que un programa tenga menos líneas de código fuente o un lenguaje necesite menos líneas para expresar lo mismo no implica que sea más legible.

A veces para escribir un buen código fuente y código legible no es necesario ni tiene que ver con utilizar patrones de diseño que en realidad añaden complejidad o complejas arquitecturas de software por capas o hexagonal, para escribir buen código fuente bastan unas pocas técnicas y prácticas muy sencillas y fácilmente comprensibles. Además, estas prácticas son aplicables a cualquier lenguaje de programación, los patrones de diseño y las arquitecturas de software aunque útiles resuelven problemas de complejidad y extensibilidad que son problemas en primera instancia diferentes a la legibilidad del código fuente.

Estás prácticas son simplemente recomendaciones a seguir como normal general, no son reglas estrictas que si no se aplican en todos los casos y siempre significa que el código esté completamente mal. Todas estas prácticas son sencillas y aplicables a cualquier lenguaje de programación.

Un nivel de indentación por método

Que un método tenga varios niveles de indentación anidados normalmente significa que las varias anidaciones realizan una tarea cuyo objetivo hay que inferir analizando el código. Además, los varios niveles de anidación al leer el código exige recordar la tarea de cada uno de los bloques. Un simple bucle anidado con dos for ya exige un esfuerzo significativo para analizar qué hace. Para facilitar la lectura y evitar los niveles de anidación se recomienda que cada método tenga como máximo un único nivel de anidación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Board {

    private String[] data;

    public String board() {
        StringBuilder result = new StringBuilder();

        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                result.append(data[i][j]);
            }
            result.append("\n");
        }

        return result.toString();
    }
}
NestedLoop.java

El refactor a aplicar es crear tantos métodos como sea necesario para que cada uno solo tenga un nivel de indentación. Esto tiene dos ventajas, por un lado el número de líneas de cada método será más pequeño que el único método original y por tanto más fácil de comprender, y por otro lado al tener que asignar un nombre al método hacer que este describa qué función realiza el código.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Board {

    private String[] data;

    public String board() {
        StringBuilder result = new StringBuilder();
        collectRows(result);
        return result.toString();
    }

    private void collectRows(StringBuilder result) {
        for (int i = 0; i < 10; i++) {
            collectRow(result, i);
        }
    }

    private void collectRow(StringBuilder result, int row) {
        for (int i = 0; i < 10; i++) {
            result.append(data[row][i]);
        }
        result.append("\n");
    }
}
UnnestedLoop.java

No usar la palabra clave del condicional else

Las sentencias condicionales son sentencias de control de flujo del programa que permiten ejecutar uno u otro bloque de código en función de una condición. Tener dos bloques de código que hacen cosas diferentes ofuscan cual es el camino que sigue el programa, cuando en el flujo del programa se añaden más combinaciones el número de caminos posible capaz de tomar el programa crece rápidamente, simplemente con dos sentencias if anidadas el número de combinaciones son cuatro. Por otro lado, si el código de los bloques if y else son grandes impide visualizar ambos bloques al mismo tiempo lo que obliga a ejercitar la mente para recordar las líneas de código de cada uno en el análisis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public BigDecimal getValue(Car car) {
   BigDecimal result;
   if (car.isNew()) {
       result = car.getNewValue();
   } else {
       if (!car.isOlderThanYears(1) && car.hasLessKilimetersThan(10000))
          result = car.getAlmostNewValue();
       else {
           if (!car.isOlderThanYears(5))
              result = car.getSemiNewValue();
           else 
              result = car.getOldValue();    
       }
   }
   return result;
};
IfElse.java

Algunos bloques else de las sentencias condicionales if son evitables usando cláusulas de guarda o guard clauses.

1
2
3
4
5
6
public BigDecimal getValue(Car car) {
    if (car.isNew()) return car.getNewValue();
    if (!car.isOlderThanYears(1) && car.hasLessKilimetersThan(10000)) return car.getAlmostNewValue();
    if (!car.isOlderThanYears(5)) return car.getSemiNewValue();
    return car.getOldValue();
}; 
GuardClause.java

Encapsular los datos primitivos

En un lenguaje orientado a objetos todos los métodos han de estar encapsulados en una clase. Los métodos manipulan los datos de la instancia de la clase lo que proporciona la encapsulación y los beneficios de la orientación a objetos. Cuando no existe una clase en la que añadir un método surgen los métodos que se insertan en una clase de utilidades, estos métodos suelen definirse como estáticos y está junto a otros lo que ocasiona baja cohesión y una agrupación de métodos no relacionados.

Para evitar crear métodos de utilidad y proporcionar una clase en la que insertar los métodos que manipulan datos la opción es crear una clase. En vez de trabajar con tipos primitivos de datos como un String o un Long que no proporcionan información del dominio del que trata la aplicación la opción es crear una clase Address, Telephone, Identifier, Email, …

Estas clases de dominio proporcionan dos ventajas, una que es el lugar en el que insertar los métodos que manipulan las propiedades y proporcionan validación de tipos. En la clase Email se insertan los métodos que manipulan direcciones y el compilador valida que un método recibe una dirección en vez de un String que podría ser cualquier dato como un nombre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class User{

    private Email email;

    public User(Email email) {
        this.email = email;
    }
}

public class Email {

    private String value;

    private Email(String value) {
        this.value = value;
    }

    public static Email of(String value) {
        if (!isValid(value)) {
            throw new Exception();
        }
        return new Email(value);
    }

    public static boolean isValid(String value) {
        ...
    }
}
EncapsulatedPrimitives.java

Encapsular las colecciones

En el mismo sentido que encapsular datos primitivos en clases que representan conceptos del lenguaje de dominio, es añadir las colecciones en una clase sin ningún otro dato de instancia para insertar los métodos que manipulan la colección.

En este ejemplo teniendo una colección de direcciones y teniendo la necesidad de conocer cuál de ellas representa la dirección principal, en el caso de que la colección no tenga su propia clase el método para obtener la dirección principal si se ubica la clase User el método queda enlazado con la clase User cuando debería estar asociado a la colección de direcciones.

1
2
3
4
5
6
7
8
public User {

    private Collection<Address> adresses;

    public Address getMainAddress() {
        ...
    }
}
Collections.java

La solución es crear una clase que represente la colección y entonces sí es posible crear el método asociado a la colección de direcciones.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public User {

    private Addresses addresses;
}

public class Addresses {

    private Collection<Adresss> collection;

    public Address getMainAddress() {
        ...
    }
}
FirstClassCollections.java

Un punto por línea de código

En los lenguajes de programación como Java el operador punto permite encapsular y acceder a miembros de una clase como propiedades y métodos. Cuando una misma línea de código utiliza varias veces el operador punto es posible que haya un problema de encapsulación de datos.

En vez de permitir que un tercero manipule un dato propio de la clase, para mantener la encapsulación la manipulación se ha de hacer a través de la clase. En este ejemplo la clase User no encapsula correctamente las direcciones como se refleja en la clase controlador.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class User {

    private Addresses addresses;

    public Addresses getAddresses() {
        return addresses;
    }
}

public class UserController {

    public void updateUser(User user) {
        user.getAddress().add(address);
    }
}
MultipleDot.java

Esto se consigue evitando los métodos getter y setter proporcionando métodos más específicos para las operaciones.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class User {

    private Addresses addresses;

    public Addresses addAddress(Address address) {
        return addresses.add(address);
    }
}

public class UserController {

    public void updateUser(User user) {
        user.addAddress(address);
    }
}
OneDot.java

Evitar abreviaturas

Las abreviaturas permiten ahorrar teclear algunos caracteres cada vez que se utiliza la versión abreviada de la palabra. Sin embargo, las abreviaturas tienen el problema de que dificultan la legibilidad del código, y el código debería ser optimizado no para ser escrito sino para ser leído. Con los entornos integrados de desarrollo que proporcionan asistencia de código en la escritura en muchos casos escribir cuesta lo mismo que escribir la versión abreviada y no abreviada de una variable o método.

Las excepción a esta regla son aquellas abreviaturas que están ampliamente aceptadas como las variables i y j como los contadores en una iteración, it par el dato de la lambda o min y max para para indicar el máximo o mínimo.

Mantener las clases pequeñas

Cuando una clase es muy grande es más difícil de entender y de mantener. Si una clase supera cierta cantidad de líneas de código es muy posible que pueda ser dividida en una o más clases más pequeñas o dividir un método de muchas líneas en varios más pequeños.

Evitar los métodos getter y setter

Los métodos getter y setter impiden mantener la encapsulación, las clases deben reflejar el dominio según Domain Driven Design. Estos métodos además impiden mantener la encapsulación de los datos y da lugar a clases anémicas que únicamente contiene propiedades y métodos get y set.

En este ejemplo para incrementar el saldo de una cuenta con métodos get y set un código como el siguiente no tiene ninguna operación de dominio. La operación para incrementar el saldo no está encapsulada en la clase Account, en cualquier otra parte del código que necesite realizar la operación de incrementar el saldo hay que realizar la misma operación. Además, de no ser muy legible la operación de actualización del saldo de la cuenta en la clase controlador, se encadena varias operaciones con el operador punto.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Account {

    private Money balance;

    public Money getBalance() {
        return balance;
    }

    public void setBalance(Money balance) {
        this.balance = balance;
    }
}

public AccountController {

    public void updateBalance(Account account, Money amount) {
        account.setBalance(account.getBalance().add(amount));
    }
}
GetterSetter.java

Añadiendo un método en la clase Account para incrementar el saldo el código es más legible. Además, el nuevo método add sería el lugar adecuado para insertar una validación por ejemplo para requerir que al invocar la operación la cantidad sea un valor positivo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Account {

    private Money balance;

    public void add(Money amount) {
        return balance.add(amount);
    }
}

public AccountController {

    public void updateBalance(Account account, Money amount) {
        account.add(amount);
    }
}
DomainMethod.java

Revisar las dependencias de las clases

Si una clase tiene muchas dependencias es muy posible que se convierta en una clase compleja y que realice varias tareas no relacionadas con poca cohesión. Una clase no debería tener muchas dependencias, una forma sencilla y rápida es analizar los imports de otras clases que utiliza una clase. Si tiene demasiados imports igual hace demasiadas cosas o tiene dependencias con cosas que no debería, por ejemplo una clase de la capa de dominio no debe tener dependencias de infraestructura.

Hay herramientas que permiten revisar las dependencias de forma automatizada para precisamente comprobar que las clases de un paquete no utilicen las de otros paquetes no deseados. Una de ellas es PMD y otra Checkstyle.

Segregar los cambios en los commits

Utilizar una herramienta de control de versiones permite conservar todo el historial de cambios del código fuente, por otro lado permite a varias personas colaborar y compartir los cambios unos con otros. Para hacer más efectivo el uso del historial o la revisión de código es aconsejable que los cambios de cada commit tengan un único objetivo, es preferible crear varios commits con cada acción de cambio que uno solo con todos los cambio con varias cosas mezcladas. Esta segregación hace posible eliminar los cambios no deseados de un commit en concreto y hace más fácil revisar los cambios realizados.

Por ejemplo, al hacer cambios es conveniente separar los cambios que arreglan un error de los cambios de formateo de código. Separar los cambios en diferentes commits requiere algo de planificación en los cambios que se desean hacer y dejar cambios para otro commit si se descubren nuevos cambios. con la herramienta de control de versiones Git una opción es utilizar git stash, otra opción es simplemente anotar un cambio para realizarlo con posterioridad al actual.

Referencia:


Comparte el artículo: