The talking bit

A blog about programming, mainly PHP, and maybe other things


Project maintained by franiglesias Hosted on GitHub Pages — Theme by mattgraham

Cositas de Orientación a Objetos con Swift

por Fran Iglesias

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 la 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 en base a 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.

Temas

php good-practices blogtober19 testing refactoring tdd design-principles legacy misc design-patterns bdd tips python ddd tools soft-skills bbdd api sql ethics testing, swift javascript