¿Tu código es DRY o es WET?

Buenas prácticas,Desarrollo

¿Tu código es DRY o es WET?

15 May , 2020  

Uno de los principios fundamentales a la hora de mejorar nuestro código, DRY — «Don’t Repeat Yourself».

Cuando escribimos código, debido a las prisas, la automatización por nuestra parte, el copiar — pegar, etc. no somos conscientes de que estamos duplicando nuestro código, escribiendo lo mismo una y otra vez. Es es el momento idóneo para preguntarse: ¿No habrá un modo mejor de hacerlo? ¿No existirá una metodología que nos permita ahorrar tiempo y dinero simplificando nuestro código y haciendo que sea más fácil de mantener y probar?. Este es justamente el concepto que está detrás de DRY.

¿Qué es DRY?

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system

Andy Hunt y Dave Thomas, «The pragmatic programmer«

Esta filosofía de definición de procesos promueve la reducción de la duplicación de nuestro código. La duplicación del código incrementa la dificultad de los cambios y la evolución posterior, dando lugar también a inconsistencias. «Don’t Repeat Yourself», resumido en una frase.

¿Qué es WET?

Las violaciones del principio DRY normalmente se denominan WET, que viene a decir «Write Everything Twice», «Write Every Time», «We Enjoy Typing» o «Waste Everyone’s Time», lo que significa que estamos repitiendo una y otra vez nuestro código, lo opuesto que encontramos en DRY.

Caso práctico

Vamos a ver una demostración de cómo un código simple se puede convertir en WET y como podemos solventar sus problemas para que vuelva a ser DRY.

Un código simple

Partimos de un código escrito en Python que nos muestra las horas de amanecer, salida del sol, medio día, puesta de sol, etc. en Madrid, España. El código se muestra a continuación:

from datetime import datetime
from astral import LocationInfo
from astral.sun import sun
from dateutil import tz

city = LocationInfo("Madrid", "Spain", "Europe/Madrid", 44.419177, -3.703295)
print(f"Information for {city.name}/{city.region}")
print(f"Timezone: {city.timezone}")
print(f"Latitude: {city.latitude:.02f}; Longitude: {city.longitude:.02f}\n")

s = sun(city.observer, date=datetime.now())

from_zone = tz.gettz('UTC')
madrid_zone = tz.gettz('Europe/Madrid')

print(f'Dawn Madrid time: {s["dawn"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Sunrise Madrid time: {s["sunrise"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(f'Noon Madrid time: {s["noon"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Sunset Madrid time: {s["sunset"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(f'Dusk Madrid time: {s["dusk"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')

Nuestro código muestra una salida como esta:

Information for Madrid/Spain
Timezone: Europe/Madrid
Latitude: 44.42; Longitude: -3.70

Dawn Madrid time: 14/05/2020 06:14:16
Sunrise Madrid time: 14/05/2020 06:48:32
Noon Madrid time: 14/05/2020 14:11:09
Sunset Madrid time: 14/05/2020 21:34:27
Dusk Madrid time: 14/05/2020 22:08:53

Aparecen los problemas

Todo bien hasta ahora, ¿no?. El problema aparece cuando por las circunstancias tenemos que modificar nuestro código para que, además de estas marcas de tiempo en Madrid, nos muestre la hora a la que se producirían estos eventos en otras capitales, como por ejemplo Tokio, Shanghai, Moscú, Lisboa y Nueva York. Es en este momento cuando dejándonos llevar por el WET podemos copiar y pegar el código con un resultado como éste:

from datetime import datetime

from astral import LocationInfo
from astral.sun import sun
from dateutil import tz

city = LocationInfo("Madrid", "Spain", "Europe/Madrid", 44.419177, -3.703295)
print(f"Information for {city.name}/{city.region}")
print(f"Timezone: {city.timezone}")
print(f"Latitude: {city.latitude:.02f}; Longitude: {city.longitude:.02f}\n")

s = sun(city.observer, date=datetime.now())

from_zone = tz.gettz('UTC')
madrid_zone = tz.gettz('Europe/Madrid')
japan_zone = tz.gettz('Asia/Tokyo')
china_zone = tz.gettz('Asia/Shanghai')
moscow_zone = tz.gettz('Europe/Moscow')
lisbon_zone = tz.gettz('Europe/Lisbon')
ny_zone = tz.gettz('America/New_York')

print(f'Dawn Madrid time: {s["dawn"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Sunrise Madrid time: {s["sunrise"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(f'Noon Madrid time: {s["noon"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Sunset Madrid time: {s["sunset"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(f'Dusk Madrid time: {s["dusk"].replace(tzinfo=from_zone).astimezone(madrid_zone).strftime("%d/%m/%Y %H:%M:%S")}')

print(
    f'Tokyo time at Madrid\'s dawn: {s["dawn"].replace(tzinfo=from_zone).astimezone(japan_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Tokyo time at Madrid\'s sunrise: {s["sunrise"].replace(tzinfo=from_zone).astimezone(japan_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Tokyo time at Madrid\'s noon: {s["noon"].replace(tzinfo=from_zone).astimezone(japan_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Tokyo time at Madrid\'s sunset: {s["sunset"].replace(tzinfo=from_zone).astimezone(japan_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Tokyo time at Madrid\'s dusk: {s["dusk"].replace(tzinfo=from_zone).astimezone(japan_zone).strftime("%d/%m/%Y %H:%M:%S")}')

print(
    f'Shanghai time at Madrid\'s dawn: {s["dawn"].replace(tzinfo=from_zone).astimezone(china_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Shanghai time at Madrid\'s sunrise: {s["sunrise"].replace(tzinfo=from_zone).astimezone(china_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Shanghai time at Madrid\'s noon: {s["noon"].replace(tzinfo=from_zone).astimezone(china_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Shanghai time at Madrid\'s sunset: {s["sunset"].replace(tzinfo=from_zone).astimezone(china_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Shanghai time at Madrid\'s dusk: {s["dusk"].replace(tzinfo=from_zone).astimezone(china_zone).strftime("%d/%m/%Y %H:%M:%S")}')

print(
    f'Moscow time at Madrid\'s dawn: {s["dawn"].replace(tzinfo=from_zone).astimezone(moscow_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Moscow time at Madrid\'s sunrise: {s["sunrise"].replace(tzinfo=from_zone).astimezone(moscow_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Moscow time at Madrid\'s noon: {s["noon"].replace(tzinfo=from_zone).astimezone(moscow_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Moscow time at Madrid\'s sunset: {s["sunset"].replace(tzinfo=from_zone).astimezone(moscow_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Moscow time at Madrid\'s dusk: {s["dusk"].replace(tzinfo=from_zone).astimezone(moscow_zone).strftime("%d/%m/%Y %H:%M:%S")}')

print(
    f'Lisbon time at Madrid\'s dawn: {s["dawn"].replace(tzinfo=from_zone).astimezone(lisbon_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Lisbon time at Madrid\'s sunrise: {s["sunrise"].replace(tzinfo=from_zone).astimezone(lisbon_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Lisbon time at Madrid\'s noon: {s["noon"].replace(tzinfo=from_zone).astimezone(lisbon_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Lisbon time at Madrid\'s sunset: {s["sunset"].replace(tzinfo=from_zone).astimezone(lisbon_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'Lisbon time at Madrid\'s dusk: {s["dusk"].replace(tzinfo=from_zone).astimezone(lisbon_zone).strftime("%d/%m/%Y %H:%M:%S")}')

print(
    f'NY time at Madrid\'s dawn: {s["dawn"].replace(tzinfo=from_zone).astimezone(ny_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'NY time at Madrid\'s sunrise: {s["sunrise"].replace(tzinfo=from_zone).astimezone(ny_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'NY time at Madrid\'s noon: {s["noon"].replace(tzinfo=from_zone).astimezone(ny_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'NY time at Madrid\'s sunset: {s["sunset"].replace(tzinfo=from_zone).astimezone(ny_zone).strftime("%d/%m/%Y %H:%M:%S")}')
print(
    f'NY time at Madrid\'s dusk: {s["dusk"].replace(tzinfo=from_zone).astimezone(ny_zone).strftime("%d/%m/%Y %H:%M:%S")}')

La salida del código en este caso sería la siguiente:

Information for Madrid/Spain
Timezone: Europe/Madrid
Latitude: 44.42; Longitude: -3.70

Dawn Madrid time: 14/05/2020 06:14:16
Sunrise Madrid time: 14/05/2020 06:48:32
Noon Madrid time: 14/05/2020 14:11:09
Sunset Madrid time: 14/05/2020 21:34:27
Dusk Madrid time: 14/05/2020 22:08:53
Tokyo time at Madrid's dawn: 14/05/2020 13:14:16
Tokyo time at Madrid's sunrise: 14/05/2020 13:48:32
Tokyo time at Madrid's noon: 14/05/2020 21:11:09
Tokyo time at Madrid's sunser: 15/05/2020 04:34:27
Tokyo time at Madrid's dusk: 15/05/2020 05:08:53
Shanghai time at Madrid's dawn: 14/05/2020 12:14:16
Shanghai time at Madrid's sunrise: 14/05/2020 12:48:32
Shanghai time at Madrid's noon: 14/05/2020 20:11:09
Shanghai time at Madrid's sunser: 15/05/2020 03:34:27
Shanghai time at Madrid's dusk: 15/05/2020 04:08:53
Moscow time at Madrid's dawn: 14/05/2020 07:14:16
Moscow time at Madrid's sunrise: 14/05/2020 07:48:32
Moscow time at Madrid's noon: 14/05/2020 15:11:09
Moscow time at Madrid's sunser: 14/05/2020 22:34:27
Moscow time at Madrid's dusk: 14/05/2020 23:08:53
Lisbon time at Madrid's dawn: 14/05/2020 05:14:16
Lisbon time at Madrid's sunrise: 14/05/2020 05:48:32
Lisbon time at Madrid's noon: 14/05/2020 13:11:09
Lisbon time at Madrid's sunser: 14/05/2020 20:34:27
Lisbon time at Madrid's dusk: 14/05/2020 21:08:53
NY time at Madrid's dawn: 14/05/2020 00:14:16
NY time at Madrid's sunrise: 14/05/2020 00:48:32
NY time at Madrid's noon: 14/05/2020 08:11:09
NY time at Madrid's sunser: 14/05/2020 15:34:27
NY time at Madrid's dusk: 14/05/2020 16:08:53

Como vemos, el código funciona, pero… ¿De verdad es funcional un código tan repetitivo para una tarea tan simple? ¿De verdad necesitamos 83 líneas de código para sacar por pantalla básicamente lo mismo? ¿No podríamos simplificar nuestro código para no ser tan repetitivos? ¿Qué ocurriría si en un futuro tenemos que procesar la salida de otras 10 ciudades nuevas, volvemos a copiar y pegar el código y modificamos los nombres?

Encontrando una solución

Analicemos detalladamente el código desde una perspectiva DRY. Como podemos observar, la mayoría de la salida es básicamente la misma, tanto que podríamos asignar dos grupos: la salida relativa a Madrid y la salida relativa al resto de ciudades. Un buen sistema de simplificación, por tanto, consistiría en crear un conjunto de ciudades objetivo y realizar una función que recorriera estas ciudades objetivos para imprimir la información, simplificando así el código y estando preparados para poder añadir nuevas ciudades en el futuro. Si continuamos observando vemos que podemos utilizar este mismo procedimiento para las distintas fases solares que queremos mostrar. Incluso podemos simplificar el método de impresión. El resultado final podría ser el siguiente:

from datetime import datetime

from astral import LocationInfo
from astral.sun import sun
from dateutil import tz

city = LocationInfo("Madrid", "Spain", "Europe/Madrid", 44.419177, -3.703295)
print(f"Information for {city.name}/{city.region}")
print(f"Timezone: {city.timezone}")
print(f"Latitude: {city.latitude:.02f}; Longitude: {city.longitude:.02f}\n")

s = sun(city.observer, date=datetime.now())

from_zone = tz.gettz('UTC')
madrid_zone = tz.gettz('Europe/Madrid')
phases = ['dawn', 'sunrise', 'noon', 'sunset', 'dusk']
external_zones = [{'name': 'Tokyo', 'zone': tz.gettz('Asia/Tokyo')},
                  {'name': 'Beijing', 'zone': tz.gettz('Asia/Shanghai')},
                  {'name': 'Moscow', 'zone': tz.gettz('Europe/Moscow')},
                  {'name': 'Lisbon', 'zone': tz.gettz('Europe/Lisbon')},
                  {'name': 'NY', 'zone': tz.gettz('America/New_York')}]


def generate_secondary_text(phase, city):
    return "{city} time at Madrid's {phase}".format(city=city, phase=phase)


def printOutput(time, from_zone, to_zone, text):
    print(f'{text} {time.replace(tzinfo=from_zone).astimezone(to_zone).strftime("%d/%m/%Y %H:%M:%S")}')


for phase in phases:
    printOutput(s[phase], from_zone, madrid_zone, "{phase_cap} Madrid time:".format(phase_cap=phase.capitalize()))

for external_zone in external_zones:
    for phase in phases:
        printOutput(s[phase], from_zone, external_zone['zone'], generate_secondary_text(phase, external_zone['name']))

La salida de código es exactamente la misma que en la fase de código anterior, pero como vemos el código se ha simplificado muchísimo y se ha vuelto mucho más escalable con la simple introducción de un par de bucles. Si en el futuro hay que añadir una, dos o diez ciudades únicamente tendríamos que añadirlas a nuestro listado de zonas para que automáticamente fueran procesadas y se mostrara sus horas relativas.

Conclusiones finales

Si bien tampoco es bueno que nos obsesionemos en reducir nuestro código al absurdo, siempre es conveniente que tratemos de repetirnos lo menos posible, ya que a la larga esta filosofía se vuelve mucho más sostenible, legible y personalizable.

Es hora de que os hagáis la pregunta del título, ¿tu código es DRY o es WET? Piénsalo y actúa en consecuencia.

Puedes comentar qué tipo de soluciones DRY implementas en tu código o incluso proponer mejoras sobre el código del ejemplo utilizando los comentarios.

Recursos

He publicado el código de ejemplo en mis repositorios para que puedas descargarlo y trastear con el si te apetece:

, , ,


Deja un comentario

A %d blogueros les gusta esto: