PHP + TDD + “Generics”

16 11 2007

(Observação: Poucas coisas são tão terríveis quanto se formatar um post nesse wordpress. Todos devem ficar assim…)

“Test-Driven Development (TDD) is a software development technique consisting of short iterations where new test cases covering the desired improvement or new functionality are written first, then the production code necessary to pass the tests is implemented, and finally the software is refactored to accommodate changes. The availability of tests before actual development ensures rapid feedback after any change. Practitioners emphasize that test-driven development is a method of designing software, not merely a method of testing.”

Acredito que já conheçam TDD. O que ??? Não conhece ??? Ok, ótima hora para conhecer:

Wikipedia – TDD
Introduction to Test Driven Design (TDD)
testdriven.com
improveit – tdd
phpunit

Não será eu quem vai dizer os benefícios de se utilizar TDD, até porque não é o objetivo deste post. Vamos ver de forma bem rápida como podemos criar uma classe que tem o comportamente semelhante a generics em java. E, novamente, não será eu que vai dizer o que são generics.

1 – Os testes
Vejamos um simples teste, com os comentários:

TestGenericList.php

<?php

include 'g.class.php';
include 'invalidgenerictypeexception.class.php';
include 'genericlist.class.php';

// Classe que servira de modelo e teste para o que
// vamos desenvolver
class TestGenericList extends PHPUnit_Framework_TestCase {

    /**
    * Testa o tamanho da nossa lista
    */
    public function testSize()
    {
        // instanciamos a GenericList
        // o parametro que vamos passar indica
        // o tipo de lista que teremos, neste
        // caso uma lista de Integer (int)
        // List l = new List<Integer>()
        $list = new GenericList(G::Integer);
        $list->add(1); // adicionamos um item...
        $list->add(2); // ... e mais um ...
        $list->add(3); // ... e mais um, totalizando tres
        
        // testamos se o tamanho da nossa lista
        // realmente eh igual a 3
        $this->assertEquals($list->size(), 3);
    }

    /*
        Note que ao final do teste acima, ja temos pelo menos
        tres metodos e duas propriedades definidas:
        __construct($param):
            O construtor da classe, vai receber pelo menos um
            parametro, indicando o tipo de lista. $param
            devera ser salvo para uso futuro, entao teremos
            uma propriedade na classe para isso
        add($param):
            Adiciona um item na lista. Assim como o parametro
            passado no construtor, deveremos salvar este parametro
            para uso futuro. Entao teremos mais uma propriedade na 
            classe
        size():
            Retorna o tamanho da lista. Provavelmente vai retornar
            a quantidade de itens adicionados em add($param), que
            estao armazenados em uma propriedade da classe, que
            certamente sera um array. Pensando em sizeof() ?
    */

    /**
    * Testa uma lista de Integers se aceita adicao
    * de somente novos integers
    */
    public function testAddSameTypeInteger()
    {
        // criamos uma lista de integers
        $list = new GenericList(G::Integer);
        // adicionamos um integer
        $list->add(1);
        // se tudo deu certo, teremos um integer
        // na lista e ela vai ter um tamanho = 1
        $this->assertEquals($list->size(), 1);
    }
    
    /**
    * Testa uma lista de Strings se aceita adicao
    * de somente novas Strings
    */
    public function testAddSameTypeString()
    {
        // criamos uma lista de Strings
        $list = new GenericList(G::String);
        // adicionamos uma string
        $list->add('foo');
        // se tudo deu certo, teremos uma string
        // na lista e ela vai ter um tamanho = 1
        $this->assertEquals($list->size(), 1);
    }

    /**
    * Testa uma lista de Doubles se aceita adicao
    * de somente novas Doubles
    */
    public function testAddSameTypeDouble()
    {
        // criamos uma lista de doubles
        $list = new GenericList(G::Double);
        // adicionamos um double
        $list->add(10.1);
        // e se tudo deu certo, teremos um double
        // na lista e ela vai ter um tamanho = 1
        $this->assertEquals($list->size(), 1);
    }
    
    /**
    * Testa uma lista de Objetos se aceita adicao
    * de somente novas Objetos DO MESMO TIPO. Ou seja
    * Uma lista de "new Pessoa()" deve aceitar adicao
    * de somente "new Pessoa()"
    */
    public function testAddSameTypeObject()
    {
        // criamos um objeto que servira de
        // modelo para a lista...
        $obj = new StdClass();
        
        // ...e um objeto que sera adiciona na lista.
        // Note que ambos sao instancias de StdClass
        $obj2 = new StdClass();
        
        // criamos uma lista de objetos do tipo
        // stdclass
        $list = new GenericList($obj);
        // e adicionamos um objeto a esta lista
        $list->add($obj2);
        // se tudo deu certo, teremos um objeto
        // na lista e ela vai ter um tamanho = 1
        $this->assertEquals($list->size(), 1);

        // novamente criamos uma lista de objetos do tipo
        // stdclass, mas desta vez ao inves de passar um objeto
        // da classe, passamos o tipo de objeto que a lista
        // ira conter
        $list = new GenericList(G::type('stdclass')); // same as above, but using class name
        // e adicionamos um objeto do tipo especificado a lista
        $list->add($obj2);
        // novamente, se tudo deu certo, teremos uma
        // lista de tamanho = 1, e testamos isso
        $this->assertEquals($list->size(), 1);
    }
    
    /**
    * Aqui iremos forcar um erro no teste
    * Vamos criar uma lista de Integer e tentar
    * adicionar outros tipos. Em todos os casos
    * diferentes do tipo original, devera ocorrer
    * um erro
    */
    public function testAddDifferentTypeInteger()
    {
        // criamos a lista de integer
        $list = new GenericList(G::Integer);

        // nos casos em que vamos utilizar algo diferente
        // de integer, utilizamos bloco try/catch. O
        // comportamento da lista sera de gerar uma
        // exception no caso de adicao de um tipo diferente

        try {
            // tentamos adicionar uma string na lista de
            // integer
            $list->add('foo');
            
            // nao deveria chegar neste ponto, visto que
            // na linha acima deveria ter sido gerada. Entao
            // dizemos ao phpunit que este teste falhou
            $this->fail('Generic Integer accepted a string');
        } catch (InvalidGenericTypeException $e) {}

        try {
            $list->add(2.1);
            $this->fail('Generic Integer accepted a double');
        } catch (InvalidGenericTypeException $e) {}

        try {
            $list->add(new StdClass());
            $this->fail('Generic Integer accepted a object');
        } catch (InvalidGenericTypeException $e) {}
    }
    
    /**
    * Aqui iremos forcar um erro no teste
    * Vamos criar uma lista de Strings e tentar
    * adicionar outros tipos. Em todos os casos
    * diferentes do tipo original, devera ocorrer
    * um erro
    */
    public function testAddDifferentTypeString()
    {
        // criamos a lista de strings
        $list = new GenericList(G::String);

        // e tentamos adicionar outros tipos, do mesmo
        // modo que fazemos no teste anterior
        
        try {
            $list->add(1);
            $this->fail('Generic String accepted a integer');
        } catch (InvalidGenericTypeException $e) {}

        try {
            $list->add(1.0);
            $this->fail('Generic String accepted a double');
        } catch (InvalidGenericTypeException $e) {}

        try {
            $list->add(new StdClass());
            $this->fail('Generic String accepted a object');
        } catch (InvalidGenericTypeException $e) {}
    }
    
    /**
    * Aqui tambem iremos forcar um erro no teste
    * Vamos criar uma lista de Double e tentar
    * adicionar outros tipos. Em todos os casos
    * diferentes do tipo original, devera ocorrer
    * um erro
    */
    public function testAddDifferentTypeDouble()
    {
        // criamos a lista de Double
        $list = new GenericList(G::Double);

        // e depois eh aquilo...

        try {
            $list->add(1);
            $this->fail('Generic Double accepted a integer');
        } catch (InvalidGenericTypeException $e) {}

        try {
            $list->add('foo');
            $this->fail('Generic Double accepted a string');
        } catch (InvalidGenericTypeException $e) {}

        try {
            $list->add(new StdClass());
            $this->fail('Generic Double accepted a object');
        } catch (InvalidGenericTypeException $e) {}
    }
    
    /**
    * Novamente o erro.
    * Vamos criar uma lista de Objetos e tentar
    * adicionar outros tipos. Em todos os casos
    * diferentes do tipo original, devera ocorrer
    * um erro
    */
    public function testAddDifferentTypeObject()
    {
        $obj = new StdClass();
        // criamos uma lista de objetos StdClass
        $list = new GenericList($obj);

        try {
            // nao podemos adicionar Integer
            $list->add(1);
            $this->fail('Generic Object accepted a integer');
        } catch (InvalidGenericTypeException $e) {}

        try {
            // e nem String
            $list->add('foo');
            $this->fail('Generic Object accepted a string');
        } catch (InvalidGenericTypeException $e) {}

        try {
            // nem double
            $list->add(2.5);
            $this->fail('Generic Object accepted a double');
        } catch (InvalidGenericTypeException $e) {}

        try {
            // e nem outro tipo de objeto, neste caso uma
            // Exception
            $list->add( new Exception());
            $this->fail('Generic Object accepted a Exception');
        } catch (InvalidGenericTypeException $e) {}

        // mesma ideia, mas utilizando uma lista criada
        // com o nome da classe
        $obj = new StdClass();
        $list = new GenericList(G::type('stdclass'));

        try {
            $list->add(1);
            $this->fail('Generic Object accepted a integer');
        } catch (InvalidGenericTypeException $e) {}

    }
    
    /**
    * Neste teste vamos verificar se a nossa lista
    * aceita um objeto de uma classe diferente da
    * classe definida como o tipo da lista, mas sendo
    * uma classe "extendida":
    *
    * class Car{}
    * class Ferrari extends Car {}
    *
    * criamos uma lista de Car e tentaremos adicionar
    * uma Ferrari
    */
    public function testAddParentClassObject()
    {
        // criamos 2 classes em runtime
        // php nao aceita criacao de nested classes
        eval('class Car {}');
        eval('class Ferrari extends Car {}');
        
        $obj = new Car();
        // criamos uma lista de Car, da mesma forma que criavamos
        // anteriormente. Entao se tentarmos adicionar uma Ferrari
        // na lista, devera ser gerada uma exception
        $list = new GenericList($obj); // creates a Car's list

        try {
            // adicionar uma Ferrari em uma lista de Car
            // deve gerar uma exception
            $list->add(new Ferrari()); // must throw a exception
            $this->fail('Generic Object accepted a parent class object without G::extend');
        } catch (InvalidGenericTypeException $e) { }

        $obj = new Car();

        // agora vamos definir que a nossa lista pode
        // receber alem de Car, classes que herdem Car
        // Collection c = new Collection<? extends Car>()
        $list = new GenericList(G::extend($obj));
        // Ferrari herda Car, entao esperamos que funcione
        $list->add(new Ferrari()); 


        // fazemos a mesma coisa, mas utilizando o nome da classe
        // ao inves de um objeto
        
        $obj = new Car();
        $list = new GenericList(G::type('car')); // creates a Car's list

        try {
            $list->add(new Ferrari()); // must throw a exception
            $this->fail('Generic Object accepted a parent class object without G::extend');
        } catch (InvalidGenericTypeException $e) { }

        $obj = new Car();
        $list = new GenericList(G::extend($obj));
        $list->add(new Ferrari()); // should work
    }
}

Após terminarmos de escrever o teste, temos bem definido uma grande parte da classe que vamos implementar. Já temos pelo menos 3 métodos públicos definidos:

__construct($param):
O construtor da classe, vai receber pelo menos um
parâmetro, indicando o tipo de lista. $param
deverá ser salvo para uso futuro, então teremos
uma propriedade na classe para isso

add($param):
Adiciona um item na lista. Assim como o parâmetro
passado no construtor, deveremos salvar este parâmetro
para uso futuro. Então teremos mais uma propriedade na
classe

size():
Retorna o tamanho da lista. Provavelmente vai retornar
a quantidade de itens adicionados em add($param), que
estao armazenados em uma propriedade da classe, que
certamente sera um array. Pensando em sizeof() ?

E também pelo menos 2 propriedades.
Observando o teste notamos também mais duas classes: G, que contem os tipos disponíveis e dois métodos, e também uma classe InvalidGenericTypeException, que representa as exceptions geradas por GenericList.

2 – Implementação: g.class.php
A Classe G (de Generic), contém tres constantes – Integer, String e Double – e dois métodos estáticos – extend e type.
O método type será utilizado para informar para lista o tipo de valor que ela vai conter, sem que ela precise descobrir por si. Já o método extend será utilizado para informar a lista que o tipo a ser utilizado pode ser também subtipos. Os dois métodos vão retornar um array que será trabalhado pela lista. Vamos a implementação:

g.class.php

<?php
/**
* Classe que contem a definicao dos tipos disponiveis
*/
class G {
    /**
    * Constantes com os tipos
    */
    const Integer = 'integer';
    const String  = 'string';
    const Double  = 'double';

    public static function extend($obj)
    {
        if (!is_object($obj)) {
            throw new InvalidGenericTypeException('G::parentsOf must be called with a valid object');
        }
        return array('extend' => strtolower(get_class($obj)));
    }
    
    public static function type($val)
    {
        return array('type' => strtolower($val));
    }
}

2 – Implementação: invalidgenerictypeexception.class.php
Uma exception simples, desnecessária explicação

invalidgenerictypeexception.class.php

<?php
class InvalidGenericTypeException extends Exception {
    public function __construct($message)
    {
        parent::__construct($message);
    }
}

4 – Implementação: genericlist.class.php
Aqui temos, finalmente, a implementação da lista em si. Note que esta implementação chega a ser bem mais simples que o teste. Veja o código e acompanhe pelos comentários:

genericlist.class.php

<?php
/**
* Implementacao da lista
*
* Tem apenas proposito educacional, poderia implementar
* algum iterator, ter metodos para remover ou retornar
* um item, buscar por um item, etc.
*/
class GenericList {
    
    /**
    * Tipo de valores da lista
    */
    private $type = null;
    
    /**
    * Valores da lista
    */
    private $values = array();
    
    /**
    * Informa se o tipo deve levar em consideracao
    * tambem os subtipos
    */
    private $useExtend = false;
    
    /**
    * Construtor da classe
    *
    * Baseado no parametro recebido, inicializa as propriedades
    * $this->type e $this->useExtend
    */
    public function __construct($type)
    {
        // se o parametro for um objeto, o tipo
        // da lista sera definido para o nome da
        // classe utilizada para criacao do objeto,
        // em minusculo
        if (is_object($type)) {
            $this->type = strtolower(get_class($type));
        } 
        // se nao for um objeto, pode ser um array,
        // G::String, G::Double ou G::Integer
        else {
            // caso seja um array...
            if (is_array($type)) {
                // ... verifica se existe a chave
                // extend. Caso exista, significa que
                // o tipo foi definido por meio de
                // G::extend
                if (array_key_exists('extend', $type)) {
                    // define que estara levando em consideracao
                    // os subtipos do tipo
                    $this->useExtend = true;
                    // define o tipo
                    $this->type = $type['extend'];
                // caso exista a chave 'type', significa que o tipo
                // da lista foi definida com g::type. Apenas
                // pega o valor de type e seta como padrao para
                // a lista
                } else if(array_key_exists('type', $type)) {
                    $this->type = $type['type'];
                }
                // nao deveria chegar aqui
                else {
                    throw new InvalidGenericTypeException('Undefined type');
                }
            }
            // se nao for array, o tipo foi passado diretamente como
            // parametro. Apenas utiliza-o
            else {
                $this->type = $type;
            }
        }
    }
    
    /**
    * Adiciona um valor a lista
    */
    public function add($val)
    {
        // verifica se o tipo sendo adicionado eh um objeto
        // e se o tipo do objeto eh diferente do tipo definido
        // para a lista
        if (is_object($val) && strtolower(get_class($val)) != $this->type) {
            // caso o tipo seja diferente, verifica se a lista foi criada
            // com a opcao de se utilizar subtipos do tipo (extend)
            if ($this->useExtend != true || !($val instanceof $this->type)) {
                // caso nao tenha, gera uma exception
                throw new InvalidGenericTypeException(sprintf('Invalid object type: %s, expected: %s', strtolower(get_class($val)), $this->type));
            }
        }
        
        // caso o tipo sendo adicionado nao seja um objeto
        // e seja diferente do tipo definido para a lista,
        // gera uma exception (tambem)
        if (!is_object($val) && gettype($val) != $this->type) {
            throw new InvalidGenericTypeException(sprintf('Invalid type: %s, expected: %s', get_class($val), $this->type));
        }
        
        // se chegou ate aqui, nao ha erros
        $this->values[] = $val;
        
        // retorna o valor sendo adicionado a lista
        return $val;
    }
    
    /**
    * Retorna o tamanho da lista
    */
    public function size()
    {
        // exatamente como imaginou no teste,
        // um simples sizeof
        return sizeof($this->values);
    }
}

5 – Testando
Depois de termos as implementações, já podemos testar:

$ phpunit TestGenericList.php
PHPUnit 3.0.6 by Sebastian Bergmann.

……….

Time: 00:00

OK (10 tests)

E tudo ok. Não é comum passar 100% logo na primeira implementação, mas tudo bem.

6 – Considerações
Algumas coisas que podemos notar:

– Antes mesmo de escrever a implementação da classe GenericList já tinhamos “pronto” nos testes o comportamento que desejavamos
– Todos os testes FALHAVAM antes da implementação da classe. E falhavam de duas formas: Primeiro porque não havia a classe e nem os métodos, então havia a falha de classe não encontrada. Segundo, por mais que tivessemos a classe e os métodos, mas os métodos não tivessem sido implementados, os testes iriam falhar. Este é o comportamento correto de um teste. É comum quando se começa a escrever testes (unitários) fazer com que eles passem antes mesmo de falhar.
– Escreva um teste para falhar e não para passar
– Em PHP não poderemos (não no 5, 6 e nem tão cedo, acredito) utilizar de forma “elegante”, como se faz em outras linguagens, a GenericList como parâmetro de um método, mas podemos emular isso:
java: public void showNomes(GenericList<Pessoa> pessoas)
c#: public void ShowNomes(GenericList<Pessoa> pessoas)
php: um leve “hack”
public function showNomes(GenericList $pessoas)
{
$pessoas->getType() == ‘pessoa’ || eval(‘throw new InvalidGenericTypeException(“Expected GenericList”);’);
}
– Como escrevi ali antes, sabiamos o comportamento que desejamos para a nossa classe. Há um outro método de design que dá mais ênfase nisso. Veja:
behavior-driven.org
Behavior driven development

É isso ai. Mais, em breve.

Arquivos:
g.class.php
genericlist.class.php
invalidgenerictypeexception.class.php
TestGenericList.php

Anúncios

Ações

Information

4 responses

17 01 2008
26 06 2008
Fabiano

Muito bom!

11 08 2010
Junior

Muito bom mesmo parabens agora vou estudar com calma para utilizar em minhas aplicações está técnica parabens.

15 02 2011
Douglas

Cara meus parabéns. Conheço java e a poderosa orientação a objeto que a linguagem disponibiliza. Estou usando PHP há alguns meses e tinha dúvidas quanto às possibilidades de usar os conceitos de orientação a objeto. Agora não tenho mais. Seus posts mostram que é possível usarmos normalmente a orientação a objeto no PHP. Mais uma vez parabéns.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s




%d blogueiros gostam disto: