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 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.

Temas

good-practices

refactoring

php

testing

tdd

design-patterns

python

blogtober19

design-principles

tb-list

misc

bdd

legacy

golang

dungeon

ruby

tools

hexagonal

tips

software-design

ddd

books

bbdd

soft-skills

pulpoCon

oop

javascript

api

typescript

sql

ethics

agile

swift

java