Saltar al contenido principal
Página

Tema 3.1 - Clean Code - Código limpio

Clean Code es una propuesta hecha por Robert C. Martin, en la que describe las buenas prácticas de programación, las define como algo vital para las compañías y los programadores, porque cada vez que se escribe código desordenado, confuso y sin pruebas unitarias, otros programadores tardan mucho tiempo en la comprensión del código, incluso el mismo programador al cabo de algún tiempo, se volverá incapaz de comprender y entendersu propio código.

A continuación, hablaremos de un conjunto de reglas y principios que denotan buenas prácticas de programación, tomando como referencia el lenguaje de programación Java.


Estándar de Nombramiento

  • Paquetes: Se escribirán siempre en letra minúscula de manera que no entren en conflicto con los nombres de las clases o interfaces. El prefijo del paquete siempre corresponderá a un nombre de dominio de primer nivel (es, org, com, net, etc...). El resto de componentes del paquete se nombrarán de acuerdo a las normas internas de la empresa, es decir, se hará referencia a la jefatura, departamento, área, entre otros. Generalmente se usa el nombre de dominio de internet en orden inverso. Si el dominio de la empresa contiene un “-” éste se debe reemplazar por “_”.

Ejemplo: co.com.qvision.certificación


Para ver el detalle de este estándar vea el siguiente enlace: https://maven.apache.org/guides/mini/guide-naming-conventions.html

  • Clases e Interfaces: Los nombres de las clases deben ser sustantivos y deben tener la primera letra en mayúsculas y si el nombre es compuesto cada palabra deberá empezar por mayúscula, es decir, deben hacer uso de UpperCamelCase. Los nombres deben ser símples y descriptivos y se debe evitar hacer uso de abreviaciones o acrónimos salvo en aquellos casos en los que el acrónimo sea más utilizado que la palabra que representa. Ejemplo: URL, HTTP. Las interfaces siguen las mismas reglas que las clases salvo que hacen uso del prefijo “I” para diferenciarla de la clase que la implementa.
  • Métodos: Los métodos son verbos escritos siempre en minúsculas. Cuando se trata de una palabra compuesta la primera letra a partir de la segunda palabra irá en mayúsculas.


Ejemplo:

1
2
3
public void imprimirReporte(String informeEjecutivo){
...
} 


Variables: Las variables siempre se escriben en minúsculas y al igual que los métodos si su nombre está compuesto por varias palabras, a partir de la segunda palabra la primera letra de cada una de ellas se escribirá en mayúsculas. Las variables nunca podrán comenzar con el carácter "_" o "$". Los nombres de variables deben ser cortos y sus significados tienen que expresar con suficiente claridad la función que desempeñan en el código. Debe evitarse el uso de nombres de variables con un sólo carácter, excepto para variables temporales. 

Constantes: Todos los nombre de constantes deben escribirse en mayúsculas, cuando se trate de palabras compuestas, éstas deben ir separadas por “_”.

Nombres con sentido:  Los nombres con sentido se refieren a que se debe tener en cuenta que los nombres de variables, métodos y clases deben tener un sentido y ser descriptivo, es decir, que su nombre explique por sí solo cuál es el uso de la variable, método o clase. Es de anotar que se debe evitar abreviaciones, prefijos y números en variables.

Un ejemplo de ello sería:

1
2
3
4
5
private String primerNombre;

public String obtenerPrimerNombre() {
   return primerNombre;
}


Nótese que la variable primerNombre es descriptivo y tiene un sentido, el de guardar el primer nombre de una persona; ahora el método obtenerPrimerNombre(), también es descriptivo y con sentido ya que expresa lo que debe hacer, retornar lo que contenga la variable primerNombre.

Otro elemento importante es evitar variables con letras solas, no usar i, j, k (solo para bucles cuyo contexto sea muy específico). En vez de eso usa nombres que se puedan buscar, nombres que sean dicientes, que expresen lo que se va a guardar.

Una mala práctica se enuncia en el siguiente código:

1
2
3
private String a;
private String 1a;
int i;


Nótese que las variables a, 1a, i, no expresan lo que se quiere almacenar, no se sabe para qué están creadas, no tienen un sentido y una descripción clara de su objetivo.

Una buena práctica sería:

1
char letra;


Miremos que la variable letra, se entiende que almacenará una letra del alfabeto, es clara y se sabe cuál es el objetivo de su creación.

Ahora utiliza verbos para expresar los métodos

1
2
3
public String obtenerPrimerNombre() {
   return primerNombre;
}


Miremos que se utilizó el verbo obtener, en algunos casos se hace uso del verbo en inglés get, set, etc. Cuando se quiere acceder a los datos es una buena práctica el uso de getter y setter, que consiste en crear los métodos correspondientes con el sufijo get- y set-, para cada variable implementada. Se recomienda usar nombres técnicos cuando la intención sea técnica (Factory, Facade, etc…)

1
2
3
4
5
6
public String getPrimerNombre() {
   return primerNombre;
}
public void setPrimerNombre(String primerNombre) {
   this.primerNombre = primerNombre;
}


Funciones

Las funciones o métodos deben ser reducidas y con nombres descriptivos. Trata de evitar el anidamiento excesivo, es decir, cuando tenemos un conjunto de if anidados, es mejor hacer uso de un switch ya que está hecho para tal caso.  Un ejemplo que expresa lo anterior es:

1
2
3
4
5
6
7
8
9
public void anidamiento(char letra) {		
   if(letra == 'a') {
			
   }else if(letra == 'b') {
			
    }else if(letra == 'c') {
			
     }
}


En el método anterior, se recibe como parámetro una letra, pero se está haciendo uso de 3 condiciones if, las cuales expresan un condicional de igualación referente a la variable letra, esto es el anidamiento que se hablaba anteriormente, son un conjunto de if uno tras otro, con una condición en común, aunque funciona el código, no es una buena práctica, ya que se hace extenso y difícil de entender.

Una solución para el caso anterior es el uso de un case, éste da claridad de la lógica implementada presentando los diferentes escenarios que se pueden dar. Ahora miremos un ejemplo de ello:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void correccionAnidamiento(char letra) {		
   switch (letra) {
	case 'a':			
		break;

	case 'b':			
		break;
			
	case 'c':
		break;
			
	default:
		break;
   }
}


Notemos que ahora se hizo uso de la estructura de control case, donde se suprimió todos los condicionales if, para presentar casos de letras, esto ayuda a la legibilidad del código. Los métodos solo deben hacer una sola cosa, donde se haga lo que dice el nombre y no haya nada oculto.

A continuación se presenta un ejemplo que muestra la implementación de un método suma que realiza dicha operación y retorna su valor, pero miremos que internamente él realiza una resta, es decir, una operación que no es el objetivo por el cual el método fue creado, recordemos que cada método debe tener una única responsabilidad, no se debería estar haciendo una operación diferente a una suma, ni tener un código que no conlleve a una suma, ósea al objetivo del método.

1
2
3
4
5
public int suma(int numero1, int numero2) {
   int resultadoSuma = numero1 + numero2;
   int resultadoResta = numero1 - numero2;
   return resultadoSuma;
}


Una buena práctica es devolver excepciones en vez de códigos de error, un código de error se presenta en dos casos:

Caso1: El valor que retorna la función es cero (0), quiere decir que no se ha producido error.

Caso 2: El valor que retorna la función es diferente de cero, quiere decir que se ha producido un error, para cada tipo de error se produce un número distinto. Otra forma de código de error es retornar un valor null en un método, pero tiene un inconveniente, es propenso a una excepción de tipo NullPointerException.


Para gestionar adecuadamente los códigos de error se puede hacer uso de las excepciones del lenguaje de programación, las excepciones son un medio por el cual nos alertan de una situación anómala en el código y nos deja decidir el comportamiento que debería tener en tal caso. Para el manejo de excepciones se utiliza un bloque try/catch, donde en el catch se ingresan las excepciones. Ahora veamos un ejemplo de manejo de excepciones:

1
2
3
4
5
6
7
8
9
public boolean argumentoBooleano (boolean bool) {
	
   try {
	bool = false;
   }catch(Exception e) {
	e.getMessage();
   }
   return bool;
}


Comentarios

Los comentarios son mensajes que sólo pueden ser vistos a través de código, están denotados con //<mensaje> o /* <mensaje */, dichos mensajes no son justificados cuando el código no expresa lo que queremos decir, es decir, debe haber coherencia entre lo comentado y el código. Miremos un ejemplo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void correccionAnidamiento() {
//manejo de conexion a base de datos
   char letra;
   switch (letra) {
	case 'a':			
		break;

	case 'b':			
		break;
			
	case 'c':
		break;
			
	default:
		break;
   }
}


El código anterior es una mala práctica porque el comentario no es alusivo, no coherente con lo planteado en el código, es decir, no tiene nada que ver el comentario con el código.

Son comentarios válidos:

  • Comentarios legales, de derechos de autor
  • Comentarios informativos de lo que devuelve una función, aunque también se pueden eliminar si en el nombre de la función especificamos lo que se devuelve. A continuación se enuncia un ejemplo explicativo para observar una mala práctica
1
2
3
4
public int suma(int numero1, int numero2) {
   //retorna la suma de 2 valores		
   return numero1 + numero2;
}


En el ejemplo anterior, se comenta que el método retorna la suma de dos valores, es una mala práctica porque el nombre del método (suma()) expresa el objetivo de lo que realiza, por tanto es redundante y obvio lo que se quiere expresar.

Son comentarios no validos:

  • Comentarios dudosos en la explicación
  • Redundancia
  • Obligatorios en javadoc
  • Registro de cambios
  • Comentarios que no están en el código adyacente


Formato

El formato es una forma de estructura del código, donde el tamaño de los ficheros no debe exceder 200 líneas en promedio, y con un límite máximo de 500 líneas. Se debe tener en cuenta que la distancia vertical entre los elementos relacionados debe ser mínima. A continuación, veremos un comparativo entre una buena y una mala práctica:

Mala practica
1
2
3
4
public int suma(int numero1, int numero2) {
   //retorna la suma de 2 valores		
   return numero1 + numero2;
}


Nótese el espaciado entre las 2 variables.

Buena practica

1
2
3
4
public void espaciado() {
   int variableInt;				
   String variableString;
}

Nótese que, entre las dos variables relacionadas verticalmente, no hay espacios

Ahora hablemos de la anchura de las líneas de código, debe estar entre 80 y 120 caracteres, ya que no deberíamos hacer scroll horizontal para leer código.

Otra buena práctica es mantener las tabulaciones, es decir, se deben mantener los espacios que permitan la distinción de la siguiente línea de código, aunque la longitud del código sea mínima, lo importante es la claridad. Veamos un ejemplo:


Mala práctica

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void correccionAnidamiento() {
     char letra;
     switch (letra) {
	case 'a':			
	break;

	case 'b':			
	break;
			
	case 'c':
	break;
		
	default:
	break;
    }
}


Buena práctica

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void correccionAnidamiento() {
	char letra;
	switch (letra) {
		case 'a':			
			break;

		case 'b':			
			break;
			
		case 'c':
			break;
			
		default:
			break;
	}
}


Objetos y estructuras de datos

  1. Un objeto es una unidad dentro de un sistema informático, consta de un estado y comportamiento, y dicho comportamiento consta de datos almacenados y tareas realizables en tiempo de ejecución. Los objetos deben tener la capacidad de esconder los datos y exponer las funciones que permiten manipularlos.
  2. Una estructura de datos es una forma de organizar los datos, de tal manera que me permita manipularlos individualmente o en conjunto. Una característica de las estructuras de datos es que exponen datos, pero no tienen funciones.
  3. Al manipular objetos y estructuras de datos, se debe tener en cuenta la abstracción de los datos. Cuando nos referimos a la abstracción de los datos, es que el código tenga la capacidad de aislar los datos del contexto en el que se desenvuelve, por tanto, se debe esconder su implementación y generar una interfaz para acceder y establecer los datos.
  4. Los objetos y estructuras de datos son excelentes herramientas para hacer conexiones en el sistema, cuya conexión es una dependencia de objetos de clases, esa dependencia es común para la buena interacción de todos los componentes del sistema.
  5. Existe un principio de la programación orientada a objetos llamada la Ley de Demeter, ésta se enfoca a reducir el acoplamiento entre clases, es decir, el acoplamiento se refiere a la dependencia que hay entre unidades de un sistema, el hecho de tener un bajo acoplamiento o interdependencias es un estado ideal, más es lo que se intenta obtener para lograr una buena programación, sin embargo tener un desacoplamiento total es imposible, por tanto el objetivo final de la programación es reducir al máximo el acoplamiento entre unidades del sistema. Cuando se tiene un bajo acoplamiento se mejora la mantenibilidad de las unidades del sistema, se aumenta la reutilización de unidades del sistema, se evita que un defecto en una unidad se propague a otras, se minimiza el riesgo de alterar múltiples unidades del sistema cuando se debe alterar una de ellas.


La ley de Demeter indica como un método f de la clase C debe comunicarse con otros métodos. De tal forma que dicho método f debe invocar métodos de:

  • C
  • Un objeto creado por f
  • Un objeto pasado como argumento a f
  • Un objeto en una variable de instancia de C


Miremos ahora el código, para que veamos en la implementación cómo sería:

 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 Persona {
	
	private String nombre;
	private int dni;
	
	//Ejemplo de cumplimiento de la Ley de Demeter
	public void comer(Hamburguesa hamburguesa) {
		//Podemos llamar a  métodos de objetos pasados a nuestro método
		boolean esConCebolla = hamburguesa.llevaCebolla();
		
		if(esConCebolla) {
			//podemos acceder a propiedades del propio objeto
			System.out.println("A " + this.nombre + " le gusta la hamburguesa con Cebolla, que rico! ");
		}
		
		//podemos llamar a métodos de objetos creados dentro del método
		Bebida bebida = new Bebida();
		bebida.servir();
		
		//podemos llamar a nuestros propios metodos
		echarseUnaSiesta();
	}

	private void echarseUnaSiesta() {
		// por fin a dormir
		
	}
}


La Ley de Demeter no permite invocar métodos de objetos devueltos por ninguna de las funciones permitidas, es decir, cuando se tienen muchas llamadas a métodos concatenados o los llamados chain calls, un ejemplo de ello sería como:

  • home.getOwner().getAddress().getNumber();


Esto es una clara violación a la Ley de Demeter ya que es posible que los valores que estamos obteniendo de esas llamadas, ya existieran previamente al momento en el que accedemos a ellos. No es el hecho de tener la concatenación de los métodos lo que rompe la ley, sino el acceso a ciertos objetos ya existentes antes de la llamada.

  • Owner owner = home.getOwner();
  • Address ownerAddress = owner.getAddress();
  • Number ownerNumber = ownerAddress.getNumber();


Cuando detectamos que estamos incumpliendo la Ley de Demeter, se debe aceptar que hay problema de arquitectura en la base de nuestro proyecto, lo que implicaría cambios grandes en el mismo.


Nota

Para ver en detalle estos y muchos otros temas, te recomendamos que leas el libro Clean Code de Robert C. Martin.

Última modificación: martes, 15 de marzo de 2022, 15:35