Páginas

Saturday, January 17, 2009

TDD em cantigas de roda - parte 2

Na semana passada comecei a falar sobre abordar outros tipos de situação, não diretamente relacionadas com programação, através de testes, fazendo uma espécie de exercício de abstração.

Na última quarta, no Dojo Rio, eu conversei com o pessoal sobre o assunto. Ao invés de ter sido o aquecimento, como proposto pelo Falcão, foi apenas um bate-papo após a nossa retrospectiva. Como eu imaginava, estranharam um pouco, porém acho que consegui passar a idéia num tom sério o bastante para não me ignorarem :D

Voltando ao problema iniciado na parte 1, parei justamente após escrever um teste exercitando a presença de objetos em namespaces diferentes. Por enquanto temos o seguinte, com o último teste falhando:
# -*- coding: utf-8 -*-
u"""
paunogato.py - Implementação usando TDD da cantiga de roda "Atirei o Pau
no Gato", seguindo a idéia de Dojo Lúdico proposta por
Jorge Falcão.
Referências:
* http://lifeatmymind.blogspot.com/2009/01/tdd-em-cantigas-de-roda-parte-1.html
* http://lifeatmymind.blogspot.com/2009/01/tdd-em-cantigas-de-roda-parte-2.html

by Rodolfo Carvalho
2009/01/08
"""
import unittest

class DonaChica(object):
admirada = False

class Gato(object):
def berrar(self):
DonaChica.admirada = True

class Vila(object):
def adicionar(self, obj):
pass

class TestAtireiOPauNoGato(unittest.TestCase):
def test_admiracao(self):
dona_chica = DonaChica()
gato = Gato()

self.assertFalse(dona_chica.admirada)
gato.berrar()
self.assertTrue(dona_chica.admirada,
"A dona Chica nao se admirou com o berro do gato!")

def test_admiracao_com_outro_gato(self):
dona_chica = DonaChica()
gato = Gato()
outro_gato = Gato()
vila = Vila()
vila.adicionar(dona_chica)
vila.adicionar(gato)

self.assertFalse(dona_chica.admirada)
outro_gato.berrar()
self.assertFalse(dona_chica.admirada,
"A dona Chica admirou-se com o berro de outro gato!")

if __name__ == "__main__":
unittest.main()


Notem que eu adicionei o objeto Vila e seu método adicionar (vazio).

Agora vamos criar uma interface para disparar e capturar eventos. Primeiro vou definir um Ser, que seria uma abstração acima de DonaChica e Gato, e ajustar as heranças:

class Ser(object):
def __init__(self):
self._vila = None

def definir_vila(self, vila):
self._vila = vila

def notificar(self, evento):
print self, "recebeu o evento <%s>" % (evento,)

class DonaChica(Ser):
def __init__(self):
super(DonaChica, self).__init__()
self.__admirada = False

@property
def admirada(self):
return self.__admirada

def notificar(self, evento):
super(DonaChica, self).notificar(evento)
if evento == "berro":
self.__admirada = True

class Gato(Ser):
def berrar(self):
if self._vila is not None:
self._vila.notificar("berro")

class Vila(object):
def __init__(self):
self._seres = []

def adicionar(self, ser):
ser.definir_vila(self)
self._seres.append(ser)

def notificar(self, evento):
for ser in self._seres:
ser.notificar(evento)


Foi um passo bem longo. Tirei a idéia central daqui, porém no caso em questão todos os seres tem que ouvir os eventos de todos os outros, enquanto no exemplo da Wikipedia diversos Listeners ouvem eventos de apenas um único Subject.

Basicamente, o que aconteceu agora é que ao adicionar um ser (Gato, Dona Chica, etc) a uma vila este ser sabe a que vila ele pertence.
Assim sendo, quando um ser disparar um evento, ele avisa a vila, que por sua vez propaga o evento para todos os seres nela contidos.

Rodando os testes, temos uma supresa: nosso segundo teste test_admiracao_com_outro_gato passa, mas sofremos uma regressão — o test_admiracao falhou!
Por que será? Bem, uma coisa é certa, usando testes automatizados nós ganhamos a segurança de caminhar sem medo, pois estamos protegidos das regressões. O teste protege o código, e o que muitos não percebem: o código protege o teste!

O que aconteceu não é que o código está errado, e sim o teste. Desde que inventamos o conceito de vila, um gato e uma dona chica precisam estar na mesma vila para que um ouça o outro. Logo, precisamos consertar o primeiro teste para ficar bem parecido com o segundo:

    def test_admiracao(self):
dona_chica = DonaChica()
gato = Gato()
vila = Vila()
vila.adicionar(dona_chica)
vila.adicionar(gato)

self.assertFalse(dona_chica.admirada)
gato.berrar()
self.assertTrue(dona_chica.admirada,
"A dona Chica nao se admirou com o berro do gato!")


E finalmente estamos com tudo passando!

Tem mais um detalhe, o que acontece se quem berrar for a dona_chica? Ela não pode ser admirar com o próprio berro, mas sim com o berro de um gato!
Para ficar mais claro, não vou fazer a dona_chica berrar, mas sim adicionar um novo ser na brincadeira, um cachorro. Se um cachorro berrar, bem, a dona_chica continua no mesmo estado de não-admiração...


class Cachorro(Ser):
def berrar(self):
if self._vila is not None:
self._vila.notificar("berro")
...

def test_admiracao_com_um_cachorro(self):
dona_chica = DonaChica()
gato = Gato()
cachorro = Cachorro()
vila = Vila()
vila.adicionar(dona_chica, gato, cachorro)

self.assertFalse(dona_chica.admirada)
cachorro.berrar()
self.assertFalse(dona_chica.admirada,
"A dona Chica admirou-se com o berro do cachorro!")

Note que eu utilizei uma nova forma de adicionar seres a uma vila, e com isso precisamos refatorar:

class Vila(object):
def __init__(self):
self._seres = []

def adicionar(self, ser, *outros_seres):
ser.definir_vila(self)
self._seres.append(ser)
if outros_seres:
self._seres.extend(outros_seres)
...


E, para minha surpresa, os testes passaram! Isso quer dizer que tem algo de errado, pois eu esperava que o teste falhasse. Bem, não demorou muito para ver que eu não estava definindo a vila outros_seres. O refactoring ficou assim:

    def adicionar(self, *seres):
for ser in seres:
ser.definir_vila(self)
self._seres.append(ser)


Agora sim o teste falhou, menos mal :D
Vamos melhorar nossa comunicação de eventos, podemos passar a informação de quem está disparando o evento. Desta forma, podemos distinguir entre berros de gatos e cachorros...

class DonaChica(Ser):
...
def notificar(self, evento, origem):
super(DonaChica, self).notificar(evento, origem)
if evento == "berro" and isinstance(origem, Gato):
self.__admirada = True

E ajustamos para passar self quando cachorros e gatos berram:
class Gato(Ser):
def berrar(self):
if self._vila is not None:
self._vila.notificar("berro", self)

class Cachorro(Ser):
def berrar(self):
if self._vila is not None:
self._vila.notificar("berro", self)


Testes passando!
Podemos então refatorar. O que me incomoda bastante é a duplicação de código — as classes Cachorro e Gato tem o mesmo código! Não vejo razão para um ser qualquer não poder gritar... a não ser que seja de uma subclasse Mudo(Ser)...
Então:

class Ser(object):
...
def berrar(self):
if self._vila is not None:
self._vila.notificar("berro", self)

class Gato(Ser):
pass

class Cachorro(Ser):
pass


Testes OK.
Já estou bastante satisfeito em saber que, quando o gato berrar, dona Chica irá certamente se admirar...
Mas ainda quero poder atirar o pau no gato, sem causar sua morte...

Este post já está demasiadamente longo, então é melhor continuar depois.

No comments: