Llevo unas semanas dedicándome a aprender un poco de Swift en ratos libres.
Por alguna razón me ha dado por programar el Juego de la Vida de Conway en Swift, orientado a objetos y con TDD. De momento me muevo con torpeza en el lenguaje, pero después de varios intentos, he podido empezar a escribir código un poco decente.
Han ido saliendo algunas cosas interesantes que me gustaría comentar. Empecemos por este Value Object que se encarga de representar las coordenadas de una celda del territorio en el que se desarrolla el juego:
import Foundation
struct Coordinates: Equatable
{
let x:Int
let y:Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
func inside(board: Board) -> Bool {
return board.includes(coordinates: self)
}
func n() -> Coordinates {
return Coordinates(x:x, y:y-1)
}
func w() -> Coordinates {
return Coordinates(x:x-1, y:y)
}
func e() -> Coordinates {
return Coordinates(x:x+1, y:y)
}
func s() ->Coordinates {
return Coordinates(x:x, y:y+1)
}
func nw() -> Coordinates {
return n().w()
}
func ne() -> Coordinates {
return n().e()
}
func sw() -> Coordinates {
return s().w()
}
func se() -> Coordinates {
return s().e()
}
}
Esta struct
representa las coordenadas y tiene métodos que permiten moverse en todas direcciones, representadas con el nombre de los puntos cardinales.
func n() -> Coordinates {
return Coordinates(x:x, y:y-1)
}
func w() -> Coordinates {
return Coordinates(x:x-1, y:y)
}
func e() -> Coordinates {
return Coordinates(x:x+1, y:y)
}
func s() -> Coordinates {
return Coordinates(x:x, y:y+1)
}
Como se puede ver, al ser un Value Object los métodos no cambian las propiedades x
e y
de la instancia actual, sino que devuelven una instancia nueva.
Un detalle curioso es que los métodos para moverse en diagonal los he creado usando dos movimientos en ambos ejes:
func nw() -> Coordinates {
return n().w()
}
func ne() -> Coordinates {
return n().e()
}
func sw() -> Coordinates {
return s().w()
}
func se() -> Coordinates {
return s().e()
}
Diría que es menos eficiente, ya que se crean dos instancias cuando sería posible hacerlo con una sola, pero me ha parecido interesante porque ilustra de una forma muy sencilla la idea de reutilización de código con base en tener métodos que se pueden combinar para obtener comportamientos más complejos.
El otro punto interesante es cómo saber si unas coordenadas existen en un tablero de juego. Obviamente es el tablero el que conoce sus dimensiones y límites:
import Foundation
class Board
{
let width: Int
let height: Int
var cells = [[Cell]] ()
init<G: CellGenerator>(width: Int, height: Int, generator: G) {
var g = generator
self.width = width
self.height = height
for y in 0...self.width - 1 {
self.cells.append([])
for _ in 0...self.height - 1 {
self.cells[y].append(g.next() as! Cell)
}
}
}
func includes(coordinates: Coordinates) -> Bool {
return coordinates.x >= 0
&& coordinates.y >= 0
&& coordinates.x < width
&& coordinates.y < height
}
func liveNeighbours(square: Coordinates) -> Int {
return
countExistsAndIsAliveAt(square.n()) +
countExistsAndIsAliveAt(square.w()) +
countExistsAndIsAliveAt(square.s()) +
countExistsAndIsAliveAt(square.e()) +
countExistsAndIsAliveAt(square.nw()) +
countExistsAndIsAliveAt(square.ne()) +
countExistsAndIsAliveAt(square.se()) +
countExistsAndIsAliveAt(square.sw())
}
fileprivate func countExistsAndIsAliveAt(_ square: Coordinates) -> Int {
if !square.inside(board: self) {
return 0
}
return cellAt(square).isAlive() ? 1 : 0
}
fileprivate func cellAt(_ square: Coordinates) -> Cell {
return self.cells[square.y][square.x]
}
}
El método includes
se encarga de verificar que una coordenada se encuentra dentro de los límites del tablero.
func includes(coordinates: Coordinates) -> Bool {
return coordinates.x >= 0
&& coordinates.y >= 0
&& coordinates.x < width
&& coordinates.y < height
}
Pero, ¿qué ocurre si el contexto en el que quiero consultar eso es el de las propias coordenadas? Pues que puedo hacerlo pasándole el tablero a la coordenada para que sea el propio tablero el que lo diga:
import Foundation
struct Coordinates: Equatable
{
// ...
func inside(board: Board) -> Bool {
return board.includes(coordinates: self)
}
// ...
}
Non tengo claro si este es más un caso de double dispatch o de un visitor muy sencillo, pero es un ejemplo de un patrón de uso de objetos en el que podemos hacer que uno de ellos haga uso de un conocimiento que tiene el otro sin darle acceso a propiedades privadas.