Calistenia de objetos. Pon en forma tu código con ejercicios prácticos.
La palabra calistenia como tal hasta hace apenas tres días no la había leído ni escuchado en mi vida. Ha sido a raíz de ver un vídeo sobre refactoring en la plataforma Styde.net dónde he escuchado el término por primera vez. Un concepto muy interesante enfocado a las buenas prácticas y buen código, ¿descubrimos que significa?
¿Qué es la calistenia de objetos?
El significado de la palabra calistenia en la Rae es el siguiente:
1. f. Conjunto de ejercicios que conducen al desarrollo de la agilidad y fuerza física.
Ahora bien, entendido dentro del contexto del desarrollo de software la calistenia es un término creado por Jeff Bay en el libro The ThoughtWorks Anthology y en él se refiere a ella como a una serie de ejercicios de programación sustentados en nueve normas para hacer que nuestro código cumpla los conceptos básicos que existen detrás de un buen diseño como: alta cohesión, bajo acoplamiento, sin lógicas de negocio duplicadas, encapsulación, testeable, legible…etc. Estos conceptos teóricos que estamos acostumbrados a leer, sin embargo, hay veces que son difíciles de poner en práctica. Por ejemplo, todos podemos entender que gracias al concepto de la encapsulación nos preocupremos por ocultar los datos, la implementación, los tipos, el diseño…etc, pero ¿cómo llevamos esto a cabo?
Jeff Bay propone un reto, hacer al menos mil líneas de código siguiendo las las recomendaciones reflejadas en las nueve reglas que propone en su ensayo. No es fácil, ya que las reglas no siempre son sencillas de aplicar y tampoco es que sean reglas universales pero lo que si es seguro es que aplicarlas todo lo posible hará que nuestro código sea mucho mejor y cumpla con los conceptos básicos indicados anteriormente. El autor asegura que si esas mil líneas de código se ajustan con disciplina al cien por cien a sus nueve reglas obligará al programador a pensar en respuestas más difíciles pero que a la vez le den una comprensión mucho más rica de la programación orientada a objetos.
Estas nueve reglas son las siguientes:
1. Un solo nivel de indentación (sangrado) por método
2. No utilizar la cláusula ELSE
3. Envolver y agrupar los tipos primitivos
4. Un solo punto por línea
5. No abreviar
6. Mantener las clases pequeñas
7. No usar clases con más de dos variables de instancia
8. Utilizar clases para encapsular colecciones
9. No utilizar getters/setters
1. Un solo nivel de indentación (sangrado) por método
Muy relacionado con el Principio de Responsabilidad Única. Muchas veces nos encontramos métodos gigantes que son muy difíciles de seguir y comprender. Hay recomendaciones que se esfuerzan en delimitar el número de líneas pero a veces no es sencillo en aquellos métodos muy largos. En su lugar, podríamos asegurarnos de que cada método haga una sola cosa fijando el límite en tener una única una estructura de control, un bloque de declaraciones…etc., manteniendo el mismo nivel de abstracción dentro del método. Si anidamos estructuras de control en un método eso significará que estamos trabajando en múltiples niveles de abstracción y que estaremos haciendo más de una cosa. A medida que escribamos métodos que hacen exactamente una cosa tendremos artefactos en nuestra aplicación más pequeños y el nivel de reutilización será mayor. Mantener un solo nivel de indentación nos proporcionará un límite físico y visual para intentar asegurarnos que escribimos métodos más sencillos con una única responsabilidad.
Antes:
class Board {
public String board() {
StringBuilder buf = new StringBuilder();
// 0
for (int i = 0; i < 10; i++) {
// 1
for (int j = 0; j < 10; j++) {
// 2
buf.append(data[i][j]);
}
buf.append("\n");
}
return buf.toString();
}
}
Después:
class Board {
public String board() {
StringBuilder buf = new StringBuilder();
collectRows(buf);
return buf.toString();
}
private void collectRows(StringBuilder buf) {
for (int i = 0; i < 10; i++) {
collectRow(buf, i);
}
}
private void collectRow(StringBuilder buf, int row) {
for (int i = 0; i < 10; i++) {
buf.append(data[row][i]);
}
buf.append("\n");
}
}
2. No utilizar la cláusula ELSE
Casi todos nos hemos encontrado más de una vez con código if-else anidados difíciles de entender. Además, los condicionales suelen ser fuente de duplicación de código. El siguiente ejemplo es un caso bastante optimista en el que podríamos evitar usar if-else con una cláusula de guarda.
Antes:
public void login(String username, String password) {
if (userRepository.isValid(username, password)) {
redirect("homepage");
} else {
addFlash("error", "Bad credentials");
redirect("login");
}
}
Después:
public void login(String username, String password) {
if (userRepository.isValid(username, password)) {
return redirect("homepage");
}
addFlash("error", "Bad credentials");
return redirect("login");
}
Los if-else más complicados que pueden llegar a ser sentencias switch encubiertas, pueden solucionarse de forma más elegante aplicando polimorfismo (con el patrón estrategia) o usando estructuras de datos como un mapa o un array clave-valor.
3. Envolver y agrupar los tipos primitivos
Un tipo int en sí mismo es solo un escalar sin significado. Cuando un método recibe un int como parámetro, es el nombre del método el que necesita hacer todo el trabajo para expresar la intención del mismo. Si, por ejemplo, en vez del tipo int el mismo método recibe un objeto Hora como parámetro, será mucho más fácil ver lo que está sucediendo.
Estos pequeños objetos más semánticos nos ayudarán a que nuestras aplicaciones sean más fáciles de mantener ya que damos al programador información adicional sobre cuál es el valor y porqué está siendo utilizado.
Si nuestras primitivas tienen comportamiento entonces quizás tenga más importancia este punto ya que evitaremos caer en el anti-patrón Primitive Obsession.
4. Un solo punto por línea
Esta recomendación se refiere al encadenamiento de llamadas a métodos con puntos o bien con el signo ->, por ejemplo:
...
current.addTo.list().set(buf);...
$this->class->teachers->list->push($element)
Siguiendo esta estrategia a veces es difícil saber qué objeto debe asumir la responsabilidad para una actividad. Si encontramos líneas de código con múltiples puntos (varios encadenamientos), empezaremos a encontrar muchas responsabilidades fuera de lugar ya que es muy probable que la tarea esté sucediendo en el lugar equivocado.
Entra en juego aquí la Ley de Demeter (“habla solo con tus amigos”), la cual puede ser una buena guía para depurar aquellos encadenamientos de llamadas a métodos fuera de nuestro alcance.
5. No abreviar
Relacionado con el naming. A menudo estamos tentados a abreviar en los nombres de las clases, los métodos o las variables. Debemos resistir esa tentación. Las abreviaturas pueden ser confusas y tienden a esconder problemas más grandes. ¿Por qué abreviamos? Si es porque estamos repitiendo una palabra una y otra vez tal vez nuestro método o variable se usen demasiado y haga falta eliminar código duplicado o favorecer alguna abstracción. Si es porque el nombre es demasiado largo, tal vez tengamos métodos o clases que hagan más de una cosa o es signo de alguna responsabilidad extraviada.
Deberemos mantener los nombres de clases y métodos en una o dos palabras, y evitar nombres que dupliquen el contexto, es decir, evitaremos la sobrecualificación en el nombrado de los mismos. Por ejemplo, si tenemos la clase Order, no necesitamos llamar a un método shipOrder (). Simplemente nombraremos al método como ship ().
6. Mantener las clases pequeñas
Es difícil definir que es pequeño o grande, pero por la propia experiencia de cada uno se debiera ser capaz de saber si una clase excede o no de un tamaño considerable para entenderse de forma sencilla.
Debemos hacer las clases lo más pequeñas que nos sea posible y agrupar comportamientos reutilizables en paquetes coherentes con su contenido y su comportamiento.
7. No usar clases con más de dos variables de instancia
O más de dos atributos. Esta es sin duda la regla más complicada de llevar a cabo. La mayoría de las clases deberían simplemente ser responsables de manejar una sola variable de instancia o dos como mucho. Agregar un nuevo estado a una clase hace que disminuya la cohesión de esa clase. En general, mientras programamos bajo estas reglas, nos encontramos con que hay dos tipos de clases. Las que manejan y mantienen el estado de una sola variable de instancia y aquellas que coordinan dos variables separadas. En general, no debiéramos de mezclar los dos tipos de responsabilidades.
8. Utilizar clases para encapsular colecciones
Cualquier clase que contenga una colección no debería contener ningún otro atributo en su estado y ningún otro comportamiento que no tenga que ver con el objeto de la colección a la que sostiene, es decir, cada colección debe quedar encapsulada con su estado y comportamiento adecuados.
9. No utilizar getters/setters
Esta regla sigue el principio Tell, Don’t Ask (pide, no preguntes). Se trata de consultar o establecer el estado de un objeto de una manera más semántica y, sobre todo, pasar la lógica al objeto que contiene la información, en lugar de extraer de él la información y procesarla fuera.
Y hasta aquí las nueve reglas que propone Jeff Bay para mejorar nuestras capacidades y habilidades como programador en base a ejercicios con nuestro código. Aplicando estas nueve reglas, el objetivo final es hacer un código que exprese de manera concisa abstracciones simples y elegantes para trabajar con la complejidad accidental que nos encontramos en nuestro día a día.
Chimpún.