Letrita a letrica
- S – Singleton
- T – Tight coupling
- U – Untestability
- P – Premature optimization
- I – Indescriptive naming
- D – Duplication
La S : Singleton
El patrón singleton consiste en instanciar una unica vez una clase y utilizarla en toda nuestra aplicación. Esto, en principio, no es una mala práctica, ya que evita muchos problemas en algunos casos.
Un ejemplo muy habitual del uso de este patrón es el acceso a base de datos. Normalmente nos interesa instanciar una unica vez la conexión y usarla siempre que lo necesitemos, así evitamos problemas de conectividad, sobre cargar el pool de conexiones, tener bloqueos por accesos… etc.
El problema es cuando se hace un mal uso de este patrón, ya que hace mucho más complejo el testing de la aplicación, ya que no podemos probar cada parte del código por separado al estar dependiendo de un punto global. Además estamos acoplando todo nuestro código a este patrón.
La T : Tight coupling
Tener código fuertemente acoplado es siempre un problema a la hora de mantenerlo. ¿Y que significa tener código fuertemente acoplado? Pues básicamente podemos detectarlo si, en caso de tener que cambiar una parte del código, tenenmos que hacer cambios en muchos más sitios.
Veamos un ejemplo de esto. Supongamos que estamos programando el control de un invernadero y ahora nos toca controlar el grado de humedad. Para esto necesitamos calcular la humedad relativa y en función de esto activar o desactivar determinados elementos.
def calcular_humedad_relativa(punto_rocio, temperatura):
return 6.112 punto_rocio^(17.67 * temperatura/(temperatura + 243.5))
def calculo_punto_rocio(temperatura, presion):
# calculo del punto de rocio
def activar_rociadores():
# Codigo que activa el sistema de rociadores de agua
def activar_ventilacion():
# Codigo que activa la ventilación de aire
def medir_temperatura():
# código para medir la temperatura actual
def medir_presion():
# código para medir la presión
def control_humedad():
'''
Control de la humedad dentro del invernadero
- Leemos la temperatura actual
- Leemos la presión actual
- Calculo del punto de rocio
- Calculo de la humedad relativa
- Activar rociadores o ventilacion si es necesario
'''
temperatura = medir_temperatura()
presion_atmosferica = medir_presion()
punto_rocio = calculo_punto_rocio(temperatura, presion_atmosferica)
humedad_relativa = calcular_humedad_relativa(punto_rocio, temperatura)
if(humedad_relativa < 50):
activar_rociadores()
if(humedad_relativa > 95):
activar_ventilacion()
sleep(1000)
desactivar_todo()
Parece sencillo ¿no? Y no tiene porque no funcionar, a fin de cuentas, estamos montando todo ahora. Hemos creado todas las funciones haciendo que devuelvan y reciban lo que necesitamos para poder usarlas. Las hemos hecho basandonos en lo que podemos obtener de los sensores… etc. Pero ¿qué pasa si en el futuro alguien modifica calcular_humedad_relativa para que devuelva un valor que no sea un porcentaje? ¿o si el sensor cambia y ahora en lugar de devolver la temperatura en celsius la devuelve en kelvin?. En el mejor de los casos todo el control fallará de forma total y nos daremos cuenta. En el peor el sistema se comportará de forma inesperada matando todo lo que hay dentro del invernadero. Y todo esto se debe a que hemos acoplado por completo la función de control a lo que están devolviendo esas funciones en ese momento, usando esos input.
¿Cómo podemos evitarlo? Bueno, siempre hay varias opciones, pero en este caso una que podría ser interesante es no devolver directamente los valores, sino hacer uso de getters y setters que cumplan con el compromiso de devolver la información de una determinada forma. Veamos rápidamente como podría quedar:
def control_humedad():
'''
Control de la humedad dentro del invernadero
- Leemos la temperatura actual
- Leemos la presión actual
- Calculo del punto de rocio
- Calculo de la humedad relativa
- Activar rociadores o ventilacion si es necesario
'''
temperatura = get_temperature_in_celsius()
presion_atmosferica = get_pression_in_pa()
punto_rocio = calculo_punto_rocio(temperatura, presion_atmosferica)
humedad_relativa = calcular_porcentaje_humedad_relativa(punto_rocio, temperatura)
if(humedad_relativa < 50):
activar_rociadores()
if(humedad_relativa > 95):
activar_ventilacion()
sleep(1000)
desactivar_todo()
Al usar estas funciones, estamos creando el compromiso de que siempre se tiene que devolver en porcentaje (en el caso de la humedad relativa), la temperatura siempre tiene que volver en celsius y la presión en pascales. Estamos garantizando que otras partes del código puedan ser tocadas sin comprometer esta función siempre y cuando se mantenga el compromiso de no pervertir esos getter para que devuelvan otra cosa, pero en ese caso ya no sería un problema de código acoplado, sino un error al hacer cambios en esas funciones que roza la negligencia.
Y ya, si queremos ir un paso más allá y aplicar SOLID podríamos decir que todo esto inclumple el principio de responsabilidad única, ya que esta realizando acciones que no debería corresponder a esta función. Pero esta optimización ya te la dejo a ti. Me conformo con que veas que ahora se puede cambiar el resto del código sin preocuparte por como afecte a este control.
La U : Untestability
Los test… esa gran asignatura pendiente de los desarrollos y los desarrolladores. En muchos proyectos no existen, en otros se van abandonado según se acerca la fecha de entrega y necesitamos dedicarle más tiempo a las funcionalidades que tenemos que entregar. El “hay que poner foco en lo que aporta valor al cliente” es la frase que más creación de test a matado. No tengo pruebas, pero tampoco dudas.
Pues bien, pese a que no es fácil de defender en una reunión, y no es algo que luzca en un ppt o en una demo, la realidad es que los test SÍ aportan valor. Aportan el valor de estar seguros de que podemos hacer cambios sin generar un problema en el servicio. Nos aseguran que podemos refactorizar el código sin peligro. Y nos permiten crear un sistema de CI/CD ágil y robusto que nos permita adaptar y evolucionar nuestras soluciones de software.
No es fácil crear una buena batería de test, pero la tarea se vuelve imposible si nuestro código directamente es intesteable, lo cual puede ocurrir si por ejemplo hemos incumplido el punto anterior y tenemos un código tan acoplado que es imposible probar una parte sin invocar otra. O si nuestras funciones son tan complejas que los test requieren de multiples casuisticas y complejidad para poderse ejecutar.
Hacer código testeable significa que también vamos a tener código sencillo de entender, con poco acoplamiento, mantenible y que tendremos muchos menos sustos al hacer un pase a producción.
La P : Premature optimization
Muchas veces caemos en la trampa de intentar mejorar antes de ver si lo que tenemos ya funciona. Esto nos lleva a dedicar tiempo y esfuerzo (y dinero) a tareas que quizá podamos aprovechar mejor en otras cosas (¡como montar una buena batería de test!)
Un ejemplo claro de esto podría ser - y seguro que os ha pasado - cuando en mitad de un desarrollo hay una subida de versión de una librería de las que estamos usando, que además no es retrocompatible y requiere que hagamos cambios significativos en la forma de implementarla. Y de repente, por ir a esta última versión, paramos la evolución y nos dedicamos a eso, solamente por tener la última versión. En desarrollos más largos puede pasar también que un framework se ponga de moda y de repente se cambie todo para poder utilizaro. Y en casos más extremos he llegado a ver cómo se empezaba desde cero un desarrollo porque un lenguaje nuevo de repente se hacia popular y todo el mundo veía clarisimo que era el que había que usar.
En este caso, lo que nos dice esta letra del acronimo es que todo eso está muy bien y que seguro que hay que hacerlo (a fin de cuentas, esta diciendo no a la optimización PREMATURA, no a la optimización en general). Pero primero valida lo que estas haciendo. Lanzalo con las versiones que estas usando (salvo que haya un verdadero motivo de peso para no hacerlo, como una brecha en la seguridad). Lanzalo con el lenguaje que estas usando. Incluso lanzalo sin todas las optimizaciones que tienes en la cabeza. Y cuando hayas probado que realmente funciona y hace lo que debería hacer. Y que los usuarios lo utilizan como se supone que lo tienen que utilizar. Entonces puedes empezar a plantearte emprender todos esos cambios.
La I : Indescriptive naming
Algo tan fácil cómo inexplicablemente poco interiorizado por muchos desarrolladores: Llama a las cosas con un nombre que haga sencillo entender de que se trata. Si vas a guardar en un array nombres de varias frutas, llama a ese array frutas o nombresDeFrutas pero no lo llames F, porque por mucho que creas que es evidente y te acordarás, no vas a hacerlo.
Lo mismo se puede aplicar a los nombres de las clases, funciones… usa nombres que sean fáciles de asociar con la acción que realizan, si tu función se encarga de hacer una suma, perfecto, llamalá suma da igual que al entrar en ella quede claro lo que hace, si el nombre es descriptivo no tendrás que entrar en ella. Por el contrario, si la llamas op1 de operación 1 o opM1_ç de operación mátematica 1 no te quedará más remedio que ir a ver qeu hace cada vez que te la encuentres en el código.
No te cobran por letra, no uses abreviaturas. Y da igual que lo que vayas a guardar en una variable, por ejemplo, vaya a estar unas decimas de segundo. No lo llames tmp/temp/temporal. Ni aux. Ni nada por el estilo. Se trata de que una persona pueda entender lo que esta ocurriendo ahí usando la menor cantidad de neuronas posible. No cuesta nada ponerle un nombre en condiciones que sea descriptivo y autoexplicativo.
Si quieres saber más sobre cómo poner buenos nombres en tu código, te recomiendo que veas esta entrada
La D : Duplication
No dupliques código. No, enserio, no lo hagas. Es terrible. No solo porque pierdes tiempo en hacerlo, eso es casi lo de menos. El mayor problema es a la hora de mantener ese código. Si quieres cambiar la forma en la que se hace algo, tendrás que tocarla en todos los sitios dónde se hace. Y siempre te dejarás alguna, no falla.
Sobre este tema tenemos una entrada a parte, sobre el acronimo DRY . Pero si no quieres verlo, quedate simplemente con la idea de que si en algún momento sientes que estas implementando algo que te suena haber hecho ya, tomate el tiempo de ver dónde esta y la forma de abstraerlo en una función que puedas llamar desde todos los sitios dónde necesites realizar esa acción. Tu yo del futuro - o peor, a quien le toque mantener tu código - te lo agradecerá.