¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
dev
Ismail Ahmedov 19/04/2023 Cargando comentarios…
En el artículo anterior, TDD: la clave para resolver eficientemente la kata Mars Rover, hicimos el diseño y la implementación de la kata Mars Rover. La solución ha sido muy elegante y eficiente, pero mejorable. Tal y como hemos hablado en el artículo sobre Lo bueno, lo feo y lo malo en el diseño de software, debemos eliminar los “olores feos” (code smells) y usar patrones de diseño.
Ahora veremos cómo se puede mejorar nuestra solución de la kata Mars Rover aplicando los patrones de diseño State, Command y Abstract Factory, sin olvidar la importancia de la inmutabilidad.
Revisando los métodos de los comandos, vemos que son void (que no tienen tipo de retorno), y un método void puede tener solo una de las siguientes dos razones para estar en nuestro código:
En nuestro caso, estos métodos son necesarios, lo cual implica que no son puros. El término “funciones puras” viene de la programación funcional y significa que la función no tiene efectos secundarios y siempre devuelve el mismo resultado para los mismos argumentos de entrada. Por ejemplo, si invocamos el método max() con los mismos parámetros, siempre devolverá el mismo resultado independientemente de cuantas veces se invoque:
En nuestro caso no tenemos resultado de la operación.
Si revisamos nuestros tests, vemos que estamos verificando el estado del Rover inicial, porque las ejecuciones de los comandos no devuelven resultado, en vez de esto están mutando el objeto original y esto no me hace sentir seguro. Para mí, la ejecución de la acción del test debe devolver resultado, el cual se debe usar en la verificación. Esto me da indicios de que algo no está del todo bien. Vamos por pasos. Primero resolvemos el problema que los comandos no devuelve resultado.
¿Pero cuál puede ser el problema?
Hay un concepto poderoso y simple en programación que está realmente infrautilizado: la inmutabilidad.
Básicamente, un objeto es inmutable si su estado no cambia una vez que se ha creado el objeto. En consecuencia, una clase es inmutable si sus instancias son inmutables.
Hay un argumento decisivo para usar objetos inmutables: simplifica drásticamente la programación concurrente.
Haciendo el Rover inmutable, resolveremos el problema de los "tests con mal olor".
Manos a la obra: haremos que Rover sea inmutable modificando la anotación de Lombok a @Value. Así quedaría nuestra clase Rover:
Y los tests verificarán el valor de actual:
Ya tenemos los test bien, pero vamos a revisar la clase Rover. En el artículo anterior, repasamos Los principios SOLID, ¿cuáles son y cómo pueden ayudarte?
Revisando la clase Rover, detectamos el code smell Shotgun Surgery y que no respetamos los principios de Single responsibility principle y Open / closed principle.
¿Cómo se puede detectar esto?
Para solucionar estos problemas, implementaremos el patrón de diseño State (hemos hablado de él en el artículo Lo bueno, lo feo y lo malo en el diseño de software) El patrón State permite que un objeto altere su comportamiento cuando cambie su estado interno. Esto puede dar la impresión de que el objeto cambia de clase. Por ejemplo, un interruptor tiene dos estados: “encendido” o “apagado”. Cuando el botón está en el estado “encendido” (es una instancia de la clase LightOn que extiende la clase abstracta LightSwitch), la luz está encendida y cuando el botón está en el estado “apagado” (cambia su tipo a LightOff que también extiende de la clase abstracta LightSwitch), la luz está apagada.
Este es el ejemplo de la implementación básica del código del interruptor:
Con esta simple implementación del patrón State vemos que el código es simple y nos permitirá definir nuevos estados y comportamientos de manera sencilla. Por ejemplo: si tenemos unas lámparas fluorescentes, su funcionamiento tiene 3 estados: Arranque, Encendido y Apagado.
En el estado del arranque, cuando se aplica la tensión eléctrica, la lámpara fluorescente necesita una corriente alta para encenderse, y un circuito auxiliar proporciona una alta tensión para ionizar el gas dentro del tubo y arranca la lámpara.
Si cambiamos los requisitos del programa del interruptor, añadiendo este nuevo estado diciendo que el interruptor tiene tres estados y para que la luz se encienda, debe pasar por el estado del arranque… Es muy fácil añadir este nuevo estado.
Esta sería la solución:
Vamos a aplicar el patrón State a nuestra solución de Mars Rover utilizando la técnica de refactoring "Replace Type Code with Subclasses", que implica crear subclases para cada valor del tipo codificado. Después extraeremos los comportamientos relevantes de la clase original y los trasladaremos a estas subclases. De esta manera, lograremos reemplazar el código del flujo de control por polimorfismo y mejorar la flexibilidad y mantenibilidad de nuestro código.
En esta solución, modificaremos el Rover que sea una clase abstracta que será extendida por los 4 estados que tendría: posicionado hacia los cuatro puntos cardinales de la brújula.
Creamos un package llamado rover y movemos en él la clase Rover. Duplicamos la clase Rover (copiar y pegar el fichero en la misma carpeta) y creamos la clase RoverPositionedEast con el mismo código.
Modificamos RoverPositionedEast para que extienda de la clase Rover.
Ahora duplicamos la clase RoverPositionedEast y creamos el resto de las implementaciones del estado con el mismo código: RoverPositionedNorth, RoverPositionedSouth, RoverPositionedWest.
Convertimos el Rover en clase abstracta que contiene los atributos y proporciona un constructor protected para que sea utilizado solo de las clases que la extienden. También eliminamos las implementaciones de los comandos. Así quedaría el código de la clase Rover:
Se podría usar las anotaciones de Lombok para hacer, pero para mayor legibilidad del código usamos solo la anotación @EqualsAndHashCode.
El IDE nos advierte que esto ha provocado problemas en los tests. Vamos a arreglarlos.
En vez de pasar a los tests la combinación de las coordenadas y orientación, ahora vamos a pasar el Rover con su estado. Esta es la modificación en los tests:
Vamos a configurar los estados. Empezamos por RoverPositionedEast:
Después de realizar todos estos cambios, la clase RoverPositionedEast quedará así:
Aplicando los mismos cambios para el resto de las clases del estado del Rover, terminamos con la implementación del patrón State.
El patrón Command encapsula una petición como un objeto, lo que nos permite parametrizar clientes con diferentes peticiones, poner en cola o registrar peticiones y admitir operaciones que se pueden deshacer. El comando logra desacoplar al objeto que invoca la operación del que sabe cómo realizarla, gracias a una clase base abstracta que asocia un receptor (objeto) con una acción (llamada al método). El método execute() de esta clase simplemente llama a la acción en el receptor.
Para todos los clientes de los objetos Command, estos son vistos como "cajas negras", simplemente invocando el método virtual execute() del objeto cada vez que se requiere el "servicio" del objeto.
Una clase Command contiene algún subconjunto de lo siguiente: un objeto, un método que se aplicará al objeto y los argumentos que se pasarán cuando se aplique el método. El método de execute() del comando hace que las piezas se unan para realizar la petición.
En nuestro caso tenemos unos comandos que están representados de un character, el switch decide qué comando se debe ejecutar dependiendo de este character.
Aplicando el patrón Command y Abstract Factory, podemos eliminar este switch. Vamos a hacerlo.
Creamos el package command donde estarán ubicados los comandos. Creamos la interfaz Command que definirá el método execute():
Ahora crearemos la clase CommandMove que implementa esta interfaz:
Crearemos de la misma manera las clases CommandTurnLeft y CommandTurnRight.
Ahora queda “pegar” todas estas implementaciones para que trabajen en conjunto. Usaremos una factoría para crear y aplicar los comandos:
Al implementar los patrones de diseño Command y Abstract Factory en nuestro código, hemos mejorado significativamente su flexibilidad y modularidad. Con el patrón Command, pudimos separar la responsabilidad de los objetos que invocan una operación de aquellos que la realizan, lo que nos permite crear objetos más modulares y fáciles de cambiar. Por otro lado, el patrón Abstract Factory nos permitió encapsular la creación de objetos relacionados en una factoría, lo que nos brinda la capacidad de cambiar fácilmente la implementación concreta de los objetos o añadir nuevas implementaciones sin afectar el resto del código.
Al detectar y eliminar los code smells, así como aplicar patrones de diseño adecuados, podemos mejorar significativamente la solidez y escalabilidad de una solución de software, lo que nos permite incorporar nuevas funcionalidades y adaptarnos a cambios en los requisitos de manera más eficiente.
Si bien algunos podrían considerar esta práctica como sobre ingeniería para este caso en concreto, aunque estoy de acuerdo con ellos, en este caso se ha utilizado como un ejemplo para demostrar las ventajas de la inmutabilidad y los patrones de diseño State, Command y Abstract Factory en el desarrollo de software robusto y eficiente.
Espero que este artículo haya sido útil para mostrar cómo estas técnicas pueden mejorar significativamente la calidad y la eficiencia del software.
Como siempre, tenéis el código en GitHub. Podéis usar la rama solution como punto de partida y la rama apply design patterns para ver la solución con commit separado para cada paso.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Usamos cookies propias y de terceros con fines analíticos y de personalización. Las puedes activar, configurar o rechazar. Configurar o rechazar.
Cuéntanos qué te parece.