Desde hace unos meses he estado lidiando con el tema de la complejidad del código. Ya sea por no percibir los antipatrones o lidiar con las fechas de entrega, terminamos añadiendo un cúmulo de líneas de código que pueden ser manipulados para no añadir más complejidad a la que ya se tiene y generar un modelo de dominio durante ese proceso. Este es el primer artículo en el que trataremos algunas ideas que pueden ayudar a hacer más mantenible el código.
Como observación, antes de iniciar un proceso de refactoring es necesario contar con una buena batería de pruebas que aseguren el comportamiento de la aplicación. En el siguiente repo dejo las versiones del proceso.
El tanque
Nuestro ejemplo iniciará con un tanque que se mueve en una sola dirección avanzando a pasos.
class Tank:
def __init__(self):
self.position = [0, 0]
def execute(self, orders):
...
def __backward(self):
...
def __forward(self):
...
Este tanque ejecuta órdenes para avanzar o retroceder.
def execute(self, orders):
for order in orders:
if order is 'b':
self.__backward()
elif order is 'f':
self.__forward()
def __backward(self):
self.position[1] -= 1
def __forward(self):
self.position[1] += 1
Command pattern
Cuando creamos un modelo de dominio siempre terminaremos con más clases. Y eso es bueno. Enriquece nuestras estructuras y nos permite razonar sobre ellas.
Es importante notar que cada patrón tiene un intención. Es importante que detectemos eso. Acá podríamos usar un Strategy o usar un Command en lugar del primero (como veremos más adelante), pero es esa intención la que nos ayudará a hallar el patrón que mejor se adapta. Así, la idea del patrón Command es la de ejecutar acciones dada una condición directa.
Iniciemos creando nuestros comandos.
class Backward:
def move(self, position):
new_position = copy(position)
new_positionp[1] -= 1
return new_position
class Forward:
def move(self, position):
new_position = copy(position)
new_positionp[1] += 1
return new_position
Como tenemos las mismas acciones que antes eran realizadas por los métodos __backward y __forward, estos no son necesarios y podemos dejar la ejecución de las órdenes como sigue
class Tank:
def __init__(self):
...
self.actions = {
'b': Backward(),
'f': Forward()
}
def execute(self, orders):
for order in order:
self.position = self.actions[order].move(self.position)
Así, actualizamos la posición a cada nueva orden.
Nueva funcionalidad: dirección
Ahora hagamos que nuestro tanque pueda girar. Una manera sencilla en que el command pattern nos ayuda con esto es la idea que a cada orden debemos actualizar el estado del tanque. Este estado está representado por la posición y la dirección.
class Tank:
...
def execute(self, orders):
for order in order:
self.position = self.actions[order].move(self.position)
self.direction = self.actions[order].turn(self.direction)
La idea es la misma, actualizamos la dirección en función de la anterior a cada nueva orden.
Esto lo logramos modificando los comandos de la siguiente manera:
class Backward:
def move(self, position):
...
def turn(self, direction):
return direction
class Left:
def move(self, position):
return position
def turn(self, direction):
if direction is 'N':
return 'W'
elif direction is 'W':
return 'S'
elif direction is 'S':
return 'E'
elif direction is 'E':
return 'N'
Con las clases Left y Right realizamos los giros del tanque.
Podríamos dudar un poco sobre la implementación de la actualización de los valores de la posición y la dirección. Pero esto no contradice la idea que tenemos que actualizar el estado del tanque después de cada comando. No obstante, es posible optimizarla encapsulando ese estado del tanque en una estructura adicional.
Sin embargo, vemos nuevamente el antipatrón en la implementación del giro.
State object pattern
Una manera de eliminar esos molestos elif
’es es representando la dirección actual por un estado que sepa cómo ir a la siguiente dirección.
class North:
def left(self):
return West()
def right(self):
return East()
Así, habiendo enriquecido nuestro dominio, podemos actualizar los métodos de giro para manejar los nuevos objetos de dirección.
class Left:
...
def turn(self, direction):
return direction.left()
class Right:
...
def turn(self, direction):
return direction.right()
De manera que no importa en que dirección nos encontremos, siempre sabremos como ir a la siguiente.
Con turbo
Imaginemos que nuestro tanque puede habilitar un modo turbo para poder ir el doble de rápido. Para ello, el método de movimiento tiene que saber si aplica el avance rápido o el normal.
class Tank:
...
def execute(self, orders):
for order in order:
self.position = self.actions[order].move(self.position, self.mode)
self.direction = self.actions[order].turn(self.direction)
Con este criterio, el método move(position, mode) recibe un argumento adicional.
class Forward:
def move(self, position, mode):
new_position = ...
new_position[1] += self.__step(mode)
return new_position
def __step(self, mode):
return 2 if mode is 'turbo' else 1
Vemos que el nuevo método __step(mode) calcula la cantidad de movimiento en función del modo en el que se encuentra el tanque.
Nuevamente, esos if
’es nos dejan una mala espina.
Strategy pattern
La idea para eliminar esos if
’es radica en que cada modo debe saber calcular la cantidad según sí mismo. Así, podemos tener dos modos que saben cuanto movimiento tienen que aplicar.
class Normal:
def step(self):
return 1
class Turbo:
def step(self):
return 2
Lo que deja a nuestro comando simple en su implementación evitando la evaluación.
class Forward:
def move(self, position, mode):
new_position = ...
new_position[1] += mode.step()
return new_position
Conclusiones
Hemos visto algunas técnicas para reducir la complejidad de nuestro código y generar un modelo de dominio a la vez. Esto no es exclusivo de un lenguaje (por eso he dejado en el repo el mismo ejemplo en un par de lenguajes adicionales). Los patrones no son algo que podamos encapsular fácilmente. Son técnicas de desarrollo para atacar problemas recurrentes. En este ejemplo hemos visto tres escenarios con dos muy similares en el abuso del if
. Recordemos que esto afecta a la complejidad del código, aunque los desaparecimos con un beneficio adicional que es creando un modelo de dominio. Enriquecer las manera en la que el código representa la realidad permite explotar el código en varios aspectos de entre los cuales resalto: la capacidad de pensar sobre el código y hallar incoherencias sin tener que programar para probarlo, y hacerlo escalable en el sentido de mantenibilidad.
Comentarios recientes