Conway’s Game of Life
He hecho la kata del Juego de la Vida de Conway de un par de maneras: inside-out y outside-in.
La primera forma ha sido TDD inside-out en Javascript con Karma, Mocha y Chai, y la segunda TDD outside-in con Groovy y Spock. Ahí dejo enlaces al Github donde se ve en cada commit qué decisiones he ido tomando siguiendo el ciclo:
ROJO -> VERDE -> REFACTOR
.
Si te interesa, para ver una buena explicación de las diferencias de hacer TDD inside-out e outside-in, puedes leer este post: TDD: Outside-In vs Inside-Out en el fantástico blog de Adictos al Trabajo, o éste (en inglés): TDD – From the Inside Out or the Outside In?. de 8th Light.
Experiencia
Me ha sido mucho más sencillo programar el primer intento. Creo que al hacerlo inside-out y siendo que lo que hay que programar es un algoritmo, fue mucho más fácil ir separando la parte del cálculo de las reglas, de la de flujo del programa.
Empecé por las condiciones que hacían que una célula viviese o no en la siguiente iteración, y poco a poco fui yendo hacia fuera hacia la composición de la malla del universo en el que vivían las células y terminando con el bucle que recorría la malla.
La segunda iteración me resultó mucho más difícil. Elegí Groovy para probar también con un lenguaje Orientando a Objetos, y así las clases me ayudasen a delimitar mejor las responsabilidades de cada componente.
Tuve en la cabeza sobre todo el Principio de Responsabilidad Única, que cada clase gestionase sólo aquello que tenía sentido para ella.
Intenté no caer en la trampa de programar abstracciones muy genéricas, pensando el típico: “para que sea fácil cambiar esto por aquello”.
Pero sí que programaba desde el inicio respetando el Principio de Inversión de Depedencias, y al inyectar siempre los colaboradores a las clases, permite hacer TDD con Mocks de manera muy transparente y natural.
// Spock test helper to create GameOfLife class GameOfLife createGol() { def nextService = Stub(NextService) def nextGrid = createSeed() nextService.next(_) >> nextGrid createGol(nextService) } GameOfLife createGol(nextService) { new GameOfLife(nextService) }
Digo ‘pero’ porque mockear todos los colaboradores antes de programarlos, para tener los tests en verde me llevó a un bug bastante difícil de cazar que explicaré más adelante.
En la revisión de código vimos que aunque los mocks ayudan a esa parte, tienes dos problemas:
- UNO: que mockear todo puede tener el efecto que realmente no estás probando nada.
- DOS: que al hacer un Stub, tienes que conocer la firma qué devuelven los colaboradores y eso produce un acoplamiento importante en fases muy tempranas.
También quiero destacar que tomé una decisión bastante interesante: el sistema no tenía por qué conocer en qué tipo de estructura de datos se almacenaba el grid en el que vivían las células y sólo se debían comunicar con el responsable a través de un API, que mostraba una topología de consenso.
Esto tiene mucho sentido, ¿verdad? Para ponerme a prueba, en vez de almacenar las células en un array bidimensional, decidí almacenarlas en un vector, en el que la matriz estaría almacenada por filas.
class Grid { int rows int columns def data = new ArrayList() ... }
Para acceder a cada posición [row, column]
, había que hacer un cálculo de paginación, en el que las filas eran la página y las columnas el offset.
GridItem get(row, column) { if (row < 0 || row >= this.rows || column < 0 || column >= this.columns) { throw new IndexOutOfBoundsException("Position requested out of grid") } return this.data[row * this.columns + column] }
No exponer las interioridades, hacía que el sistema más exterior que se encargaba de recorrer el grid, no pudiese hacer un bucle de manera normal.
Entonces se me ocurrió que podía imitar el comportamiento de un tipo iterable.
Esto es, para pedir la siguiente posición había que pedirla al objeto responsable. Esto me llevó a tener que mantener la posición de lectura en el propio objeto, y por tanto mantener estado.
Esto no me gustaba nada, pero como era una kata, seguí adelante para ver las implicaciones.
Y según avanzaba la programación encontré un bug bastante gordo relacionado con mockear todas las dependencias de los test unitarios. Si pedía el grid.next()
, y al leer avanza el cursos una posición, y luego pedía los vecinos, estaba leyendo no los que yo pensaba, sino los de la posición siguiente.
Así que tuve que crear un método que se llamase getCurrent()
, calcular los vecinos y al final usar el next()
, ya en este caso solo para avanzar posición.
class NextServiceImpl implements NextService { ... Grid next(Grid seed) { GridItem item Grid nextGrid = new Grid(seed.rows, seed.columns) // found a bug in calculating neighbours after next // we should separate this in current() and next() while (item = seed.current()) { def numNeighbours = seed.countNeighbours() def nextItem = alg.calc(item, numNeighbours) nextGrid.push(nextItem) seed.next() } return nextGrid } }
Mantener la decisión del iterable me costó bastante, la verdad.
A posteriori creo que no merece la pena en este problema abstraer cómo se almacena el sistema y después leyendo el libro, veo que expone al máximo nivel la estructura de datos, o por lo menos la topología.
Conclusión
La kata inside-out, fue mucho más satisfactoria, pues veía mucho mejor el problema que tenía que resolver y construía el sistema desde la base.
La kata outside-in me ha ayudado a ver el big-picture mucho mejor, y darme cuenta de dónde están las responsabilidades, sobre todo al hacer TDD. Como dato interesante, encontré más bugs y más difíciles de resolver.
En definitiva, ejercicio interesante. ¡Espero poder participar en el Global Day of Coderetreat el próximo 22 de octubre de 2016 desde Asturias!