Ejercicio: Mateada
Table of Contents
Enunciado
El enunciado del ejercicio está acá
Resoluciones
Veamos las soluciones que fuimos implementando iterativamente.
3 empanadas (1 foreach)
La primera solución que surgió en clase, contemplando el enunciado justo HASTA que aparezcan los personajes especiales, es decir
sólo modelando al Mateador
por defecto que toma, o pasa, sin hacer ningún truco, fue esta
class Cebador() {
def iniciarPasada(mateadores: List[Mateador]) = {
mateadores.foreach { m => m.recibirMate() }
}
}
class Mateador {
var matesTomadosEnPasada = 0
def recibirMate() {
matesTomadosEnPasada += 1
}
}
Y los tests
import org.scalatest.{ FunSpec, Matchers }
class MateadaSpec extends FunSpec with Matchers {
describe("pasada de mate") {
it("en una ronda de 1 solo mateador toma") {
val mateador = new Mateador
new Cebador().iniciarPasada(List(mateador))
mateador.matesTomadosEnPasada should equal(1)
}
it("en una ronda de 2 mateadores, toman ambos, 1 unica vez") {
val mateadores = List(new Mateador, new Mateador)
new Cebador().iniciarPasada(mateadores)
mateadores.forall { m => m.matesTomadosEnPasada == 1 } should equal(true)
}
}
}
Nos dimos cuenta que el foreach
era suficiente, ya que cumplía con la idea de darle la oportunidad de tomar
una única vez a cada participante, y, a todos.
Esto nos evitaba resolver el problema de cómo saber si un Mateador ya tomó en la pasada o no.
Es que la “pasada” la modelamos como una recorrida de la lista con foreach
.
Yo soy ‘cola’, tu pegamento (refTo monkey island)
Cuando íbamos a agregar los roles nos dimos cuenta que varios Mateadores
necesitaban intervenir cuando el mate le tocaba a otro, para tomar ellos. Es decir que necesitábamos darle el “control del flujo” a los mateadores, o al menos poder intervenir en él.
Eso al final es lo que pasa en la realidad cuando pasamos algo por una “ronda”
En la solución anterior, si quieren verlo así lo que hacíamos era que el Cebador
le diera el mate “directo” al que le correspondía. Tenía mucho más control.
Entonces se nos ocurrieron 2 opciones:
- que cada mateador conociera al siguiente, de modo de poder delegarle el mate si el no va a tomar
- cambiar el mensaje
recibirMate(mate)
de modo de que cada mateador reciba un objeto que le permita “continuar” la cadena.
Ese nuevo objeto en principio fue la lista de “mateadores a su derecha”. Es decir, el resto, o “cola” de la lista.
La solución queda:
class Mate(mateadores : List[Mateador]) {
var cebado = false
// por qué usamos un Set ?
var tomaronEnPasada = Set[Mateador]()
def cebar(): Unit = {
cebado = true
}
def teTome(mateador : Mateador) : Unit = {
tomaronEnPasada = tomaronEnPasada + mateador
cebado = false
}
def todosYaTomaron() = tomaronEnPasada.size == mateadores.size
def tomeEnEstaPasada(mateador : Mateador) = tomaronEnPasada.contains(mateador)
}
class Cebador() {
def iniciarPasada(mateadores: List[Mateador]) = {
// por qué el mate necesita conocer a los mateadores ?
var mate = new Mate(mateadores)
while(!mate.todosYaTomaron()) {
mate = mateadores.head.recibirMate(mate, mateadores.tail)
mate.cebar()
}
}
}
class Mateador {
var matesTomadosEnPasada = 0
def recibirMate(mate: Mate, siguientes : List[Mateador]) : Mate = {
matesTomadosEnPasada += 1;
return if (mate.tomeEnEstaPasada(this)) {
if (siguientes.isEmpty)
mate
else
siguientes.head.recibirMate(mate, siguientes.tail)
} else {
mate.teTome(this)
mate
}
}
}
Dejamos un par de preguntas en el código como comentarios, ya que fueron “microdecisiones” que tuvimos que tomar durante la implementación (algunas ni las llegamos a ver como la del uso del Set
)
Chain Of Responsibility
Luego planteamos que usar una List[Mateador]
, es decir una clase de Scala, es un poco limitando, o de “bajo nivel”, y si bien es “poderoso” porque permite a la impl de Mateador
acceder en forma cruda a la ronda, y por ejemplo poder saltear, etc.. también nos fuerza a repetir en cada impl el partir la lista en head
y siguientes.tail
. Es decir hacer el forwarding cada vez.
Esto
siguientes.head.recibirMate(mate, siguientes.tail)
Incluso, una cosa que no marcamos en clase es que hay una DUPLICACION. El Cebador
que es quien arranca la pasada, y también hace algo muy parecido:
mate = mateadores.head.recibirMate(mate, mateadores.tail)
Y por último, cada Mateador
hoy día también tiene que controlar si la lista que sigue está vacía, lo cual lo hace bastante molesto. Y podría considerarse parte de la lógica abstracta de “forwardear”.
Entonces un diseño más “explítico” sería modelar nuestro propio objeto que represente a la cadena.
Vamos a ver primero cómo queda el Mateador
, que es lo que queremos simplificar.
class Mateador {
var matesTomadosEnPasada = 0
def recibirMate(mate: Mate, cadena: CadenaDeMate) : Mate = {
matesTomadosEnPasada += 1;
return if (mate.tomeEnEstaPasada(this)) {
cadena.pasarMate(mate)
} else {
mate.teTome(this)
}
}
}
Como vemos queda mucho más sencillo, porque se abstrae de la propagación. Hace simplemente cadena.pasarMate(mate)
. Esto sigue permitiendo al mateador cambiar el mate (como requieren ciertos roles).
También simplificamos al Cebador
y evitamos la duplicación. Este crea la cadena y le dice “arrancá”
class Cebador() {
def iniciarPasada(mateadores: List[Mateador]) = {
var mate = new Mate(mateadores) // por qué el mate necesita conocer a los mateadores
val cadena = new CadenaDeMate(mateadores)
while(!mate.todosYaTomaron()) {
mate.cebar()
mate = cadena.pasarMate(mate)
}
}
}
Finalmente la impl de CadenaDeMate
(que si entendimos como se usa y para qué sirve, es simplemente cuestión de impl)
class CadenaDeMate(mateadores: List[Mateador]) {
var restantes = mateadores
def pasarMate(mate: Mate) : Mate = {
if (restantes.isEmpty) {
return mate
}
val proximo :: otros = restantes
restantes = otros
return proximo.recibirMate(mate, this)
}
}
Los tests debería continuar verde.
Nota: Cuando hacemos esto
val proximo :: otros = restantes
estamos “extrayendo” de la lista la cabeza (proximo
) y la cola (otros
).
Implementaciones de Personajes
// TODO strategy & decorator