terça-feira, 27 de setembro de 2011

Duff's Device

Estou postando este artigo a título de curiosidade. Embora você ainda possa encontrar uma situação prática hoje em dia em que o Duff's Device possa ser útil, em 99,9% das situações seu benefício é desprezível.

O Duff's Device é uma técnica criada por Tom Duff em 1983 para fazer uma cópia serial de dados (a mesma coisa que o memcpy faz) com um desempenho maior. Foi amplamente utilizada por progamadores de Assembly e programadores de C no começo dos tempos.

Basicamente, a maneira mais rápida de se copiar dados de uma região de memória para outra, em C, é:

void meu_memcpy( unsigned char* destino, unsigned char* origem, int quantidade )
{
     while( quantidade-- > 0 )
     {
         *destino++ = *origem++;
     }
}

O problema com esta solução é que temos que verificar se quantidade é maior que zero em cada iteração. Se estivermos copiando mil bytes, haverá mil comparações.
A solução encontrada por Duff foi diminuir este número de comparações, copiando os elementos de 8 em 8. Depois de copiar 8 elementos, ele fazia as devidas verificações e partia para o próximo bloco de cópia.
O algoritmo original do Duff era:

void meu_memcpy( unsigned char* destino, unsigned char* origem, int quantidade )
{
     register int n = (quantidade + 7) / 8;
     switch(quantidade % 8)
     {
         case 0: do { *destino++ = *origem++;
         case 7: *destino++ = *origem++;
         case 6: *destino++ = *origem++;
         case 5: *destino++ = *origem++;
         case 4: *destino++ = *origem++;
         case 3: *destino++ = *origem++;
         case 2: *destino++ = *origem++;
         case 1: *destino++ = *origem++;
             } while(--n > 0);
     }
}

Basicamente n é a quantidade de oitos que cabem em quantidade. Se der número quebrado, arredonda para cima. Ou seja, se quantidade vale 8, n vale 1. Se quantidade vale 10, n vale 2.
O switch pega o resto da divisão de quantidade por 8 para que esta quantidade seja copiada na primeira iteração. Nas demais, serão copiados 8 bytes.

Você deve ter estranhado o formato do do...while no meio dos cases, mas este código é perfeitamente válido em C.
Talvez você esteja se perguntando também porque foi usado o valor 8, e não outro qualquer.
A explicação disso é que os processadores calculam mais rapidamente qualquer divisão por valores 2^n.
Por exemplo, se quiser dividir um número por 2, basta rotacioná-lo para a direita 1 bit. Para dividir por 4, rotacione 2 bits. Para dividir por 8, 3 bits, e assim por diante.

O Duff ' Device teve sua vantagem no passado, mas hoje em dia são pouquíssimas as situações em que se vale a pena aplicá-lo. Primeiro que o código tem uma complexidade razoável perto do original. Um leigo com certeza terá dificuldades em entendê-lo.
Segundo que não necessariamente este código será mais rápido. Os compiladores atuais executam vários tipos de otimizações. Se o ganho de desempenho for justificável na sua aplicação para que você venha a adotar esta implementação, é interessante que você faça testes com o memcpy original e com a sua implementação do Duff's Device para ver se, na prática, ela realmente é mais rápida.

domingo, 25 de setembro de 2011

Aventuras com Heranças

Embora a herança em si seja um tema básico da orientação a objetos, ela possui uma série de artimanhas e regras (principalmente para quem programa em C++) que devem ser seguidas para não cair em futuras armadilhas.
Basicamente, existem três tipos de heranças em C++: a pública, a protegida e a privada. O primeiro tipo é o mais conhecido. Os outros dois não são muito utilizados e raramente são ensinados na faculdade ou nos cursos de C++. Além disso, a herança pode ser virtual ou não.
Vou explicar cada um destes tipos na sequência:

Herança pública:

De longe a mais conhecida e mais utilizada (embora muitas vezes utilizada de forma incorreta). A regra mais importante para esta herança é: quando você diz que B herda de A, você está dizendo que B é um A. Tenha isso em mente. Sempre. Só herde uma classe de outra se você puder dizer, no bom português, que a classe filha é um tipo da classe mãe. Nunca herde com o objetivo de economizar código ou coisa do tipo.
Por exemplo, você pode herdar Carro de Veiculo, Moto de Veiculo, Ferrari de Carro, Marte de Planeta, Carrefour de Supermercado, mas nunca deve herdar Moto de Carro, por exemplo. Mesmo que no seu código seja tentador fazer isso, pois a maioria das funções de Moto estão implementadas em Carro, não faça tal herança. O ideal é criar uma classe acima de Carro e Moto (como Veiculo) e colocar lá as funções em comum.
Vou citar um exemplo de um código que eu trabalhei há algum tempo que fazia o uso incorreto de herança pública.
Tínhamos uma classe chamada Thread, com um método virtual puro chamado Run, um Inicia e um SetDelay. Basicamente, se você queria ter uma thread no sistema, bastava herdar desta classe, implementar o método Run, e chamar o Inicia para que o conteúdo de Run ficasse sendo executado de acordo com o delay definido.
Como o código implementava uma comunicação por Gprs e esta comunicação deveria ficar sendo executada em uma thread separada no sistema, fizemos Gprs herdar de Thread. O problema é que Gprs não é uma Thread! Gprs pode ser considerado uma Rede, um Canal de Comunicação, mas definitivamente não é uma thread!
Apesar de parecer inofensiva, a decisão de herdar Gprs de Thread começou a causar dores de cabeça. O problema é que no código você acaba tendo algo do tipo:

Gprs* objGprs = new Gprs();
objGprs->SetDelay( 1000 );
objGprs->Inicia();

Agora, para uma pessoa que nunca mexeu no código, o que o Inicia faz? A pessoa pode imaginar que vai configurar IP, zerar as variáveis da classe, ligar o hardware do modem, o que for, mas não vai imaginar que este comando está iniciando a Thread.
Além disso, tínhamos a necessidade de ter uma função que iniciasse o modem e configurasse a rede, mas esta função não poderia se chamar Inicia porque o Inicia já está implementado em Thread.
Assim, o código acabou ficando uma beleza do tipo:

Gprs* objGprs = new Gprs();
objGprs->IniciaGprs( IP, PORTA );
objGprs->SetDelay( 1000 );
objGprs->Inicia();

O que beira a estupidez.

O correto, neste caso, seria ter criado duas classes: uma chamada ThreadGprs que herda de Thread, e outra chamada Gprs que não herda de ninguém (ou herda de CanalComunicacao, ou algo do tipo), e fazer com que ThreadGprs agregasse Gprs. Um código do tipo

ThreadGprs* objThreadGprs = new ThreadGprs();
objThreadGprs->SetDelay( 1000 );
objThreadGprs->Inicia();

Faz sentido pra qualquer novato que bata o olho nele.

Bom, dito isso, vamos ao que interessa: o que é uma herança pública?

O funcionamento é simples: a classe filha vai conseguir acessar os atributos e métodos públicos e protegidos da mãe, e as outras classes vão conseguir acessar os atributos e métodos públicos da filha e da mãe através de um objeto da filha.

Veja o exemplo:

class Base
{
public:
    void FuncaoUm()
    {
        printf( "Funcao numero 1\n" );
    }

    virtual void FuncaoDois()
    {
        printf( "Funcao numero 2 da base\n" );
    }
};

class Derivada: public Base
{
public:
    void FuncaoDois()
    {
        printf( "Funcao numero 2 da derivada!\n" );
    }
};

int main()
{
    Base* obj = new Derivada();

    obj->FuncaoUm();
    obj->FuncaoDois();

    delete obj;
    return 0;
}

O resultado disso vai ser:

Funcao numero 1
Funcao numero 2 da derivada!

(Se você não entende o que a palavra virtual faz, experimente rodar o código novamente sem ela e verá que o comportamento será diferente do esperado. Via de regra, sempre que você pretende reescrever uma função na derivada, declare a função da Base como virtual).

Função virtual pura

Se você quer obrigar a Derivada a reimplementar uma função, você pode declará-la como virtual pura. Esta função pode ou não ser implementada na Base, mas a Derivada deve obrigatoriamente implementá-la.
Uma função virtual pura é declarada da seguinte forma:

virtual void Funcao() = 0;

Veja o exemplo:

class Base
{
public:    
    //Função virtual pura não implementada.
    virtual void FuncaoPuraUm() = 0;

    //Função virtual pura implementada:
    virtual void FuncaoPuraDois() = 0
    {
        printf( "Funcao Pura Dois na base\n" );
    }
};

class Derivada: public Base
{
public:
    //Somos obrigados a implementas as funções
    //virtuais puras da base aqui, senão dá erro de
    //compilação.
    void FuncaoPuraUm()
    {
        printf( "Funcao Pura Um implementada na derivada\n" );
    }

    //Apesar de estar implementada na base, precisamos
    //implementá-la aqui também.
    void FuncaoPuraDois()
    {
        printf( "Funcao Pura Dois implementada na derivada\n" );

        //opcionalmente podemos chamar a implementação
        //da base aqui.
        Base::FuncaoPuraDois();
    }
};

int main()
{
    Base* obj = new Derivada();

    obj->FuncaoPuraUm();
    obj->FuncaoPuraDois();

    delete obj;
    return 0;
}

O resultado vai ser:

Funcao Pura Um implementada na derivada
Funcao Pura Dois implementada na derivada
Funcao Pura Dois na base

Tranquilo, né? Se você seguir corretamente a regra que eu comentei no começo, vai tirar um grande benefício com heranças, e dificilmente vai se meter em problemas.

Vamos para os tipos menos conhecidos:


Herança privada:

Um tipo de herança menos conhecido e que deve ser evitado. Diferente da pública, na herança privada, quando você diz que B herda de A, está dizendo que B tem A. Por exemplo, Carro poderia fazer uma herança privada de Motor.

A principal diferença entre esta herança e a pública, do ponto de vista técnico, é que quem possui um objeto da classe Derivada não irá conseguir chamar, por este objeto, uma função da classe Base.

Voltemos ao primeiro exemplo:

class Base
{
public:
    //Função virtual pura não implementada.
    virtual void FuncaoPuraUm() = 0;

    //Função virtual pura implementada:
    virtual void FuncaoPuraDois() = 0
    {
        printf("Funcao Pura Dois na base\n");
    }
};

class Derivada: private Base
{
public:
    //Somos obrigados a implementas as funções
    //virtuais puras da base aqui, senão dá erro de
    //compilação.
    void FuncaoPuraUm()
    {
        printf( "Funcao Pura Um implementada na derivada\n" );
    }

    //Apesar de estar implementada na base, precisamos
    //implementá-la aqui também.
    void FuncaoPuraDois()
    {
        printf( "Funcao Pura Dois implementada na derivada\n" );
        //opcionalmente podemos chamar a implementação
        //da base aqui.
        Base::FuncaoPuraDois();
    }
};

int main()
{
    //Base* obj = new Derivada();
    Derivada* obj = new Derivada();

    //obj->FuncaoPuraUm();
    obj->FuncaoPuraDois();

    delete obj;
    return 0;
}

Note as diferenças:

  1. A herança agora é private, e não mais public;
  2. Fazer um ponteiro do tipo Base apontar para um objeto alocado como o tipo Derivada já não funciona mais. (É possível utilizar um reinterpret_cast aqui para passar esta segurança, porém você já está hackeando o código e vai acabar com um comportamento inesperado mais cedo ou mais tarde);
  3. Não é mais possível chamar as funções que estão na Base através do objeto da Derivada. Assim, a linha obj->FuncaoPuraUm(); teve que ser comentada.

O mesmo vale caso você herde (independente da forma da herança) uma classe de Derivada (uma classe chamada DerivadaDaDerivada, vamos supor). DerivadaDaDerivada não conseguirá acessar nenhum método ou atributo de Base diretamente, independente se este método ou atributo é público, protegido ou privado.

Mas por que usar herança privada, e não composição ou agregação?

Difícil responder, até porque você deve utilizar composição ou agregação em vez de herança privada.
Existem duas razões para você optar por herança privada:

  1. Caso você precise que a sua classe derivada acesse os métodos e atributos protegidos da base. Isso não seria possível caso fosse utilizado composição ou agregação, a menos que você declarasse a classe agregada como friend da classe agregadora.
  2. Caso você queira declarar na base uma função virtual (tanto faz se é pura ou não) e queira que ela seja reimplementada na derivada.
Sinceramente não consigo pensar em nenhum exemplo prático bom para se utilizar este tipo de herança.
Uma situação, talvez, fosse no exemplo do Gprs que eu comentei acima. Fazer Gprs herdar de forma privada de Thread seria melhor do que fazer a herança pública, porque, desta forma, quem chamasse:

objGprs->Inicia();

Estaria, com certeza, chamando a Inicia do Gprs, e não da Thread. A diferença é que daí a responsabilidade de inicializar a thread, definir timeouts e tudo mais ficaria a cargo da classe Gprs.


Herança Protegida

Dos três tipos de herança, este é o pior. Ninguém sabe ao certo o que significa dizer que B faz uma herança protegida de A, mas está perto de ser um relacionamento do tipo B tem A, como na herança privada.

O funcionamento deste tipo de herança é bem semelhante ao da (herança) privada. Os métodos da Base continuam inacessíveis por quem tenta acessá-los por um objeto da Derivada. A diferença está em se você herdar de Derivada (o exemplo da DerivadaDaDerivada). Neste caso, a DerivadaDaDerivada conseguirá acessar os métodos e atributos públicos e protegidos da Base sem problemas.

Herança Virtual

Os três tipos citados acima podem ser virtuais ou não (embora para a herança privada eu não veja sentido).
As heranças virtuais são úteis para o caso das heranças do tipo diamante (quem programa em Java treme ao ouvir isso).
Uma herança do tipo diamante se forma quando você tem uma Base, duas (ou mais) derivadas e depois uma classe que faz herança múltipla dessas duas (ou mais) derivadas. Pense o seguinte:

class Base
{
protected:
    int x;
};

class DerivadaUm: public Base
{
};

class DerivadaDois: public Base
{
};

class Join: public DerivadaUm, public DerivadaDois
{
public:
    void Funcao()
    {
        x = 0;
    }
};

Isso vai dar um erro de compilação, dizendo que o acesso a “x” é ambíguo. Isso acontece porque Join enxerga dois x: um que veio de Base por meio da DerivadaUm e outro que veio de Base por DerivadaDois.

Para que este código funcione, é necessário que DerivadaUm e DerivadaDois façam herança virtual de Base.
Assim, o código fica da seguinte forma:

class Base
{
protected:
    int x;
};

class DerivadaUm: virtual public Base
{
};

class DerivadaDois: virtual public Base
{
};

class Join: public DerivadaUm, public DerivadaDois
{
public:
    void Funcao()
    {
        x = 0;
    }
};

Isso criará apenas uma instância de x para Join.

Bom, dizem que heranças do tipo virtual devem ser evitadas. O problema não está com as heranças virtuais, mas sim com a herança do tipo diamante. Se você chegar a um modelo que chegue a este tipo de configuração, evite-o. Modele de forma diferente. Mas, inevitavelmente, você vai acabar tendo que dar manutenção em um código que está mal modelado, e talvez esta seja a sua única saída...

sexta-feira, 23 de setembro de 2011

A palavra mágica é: explicit!

Observe a seguinte classe:

class NumeroEstupido
{
public:
    NumeroEstupido( int num ): mNumero( num ){}
    ~NumeroEstupido(){}
    int GetNumero() const { return mNumero; }
private:
    int mNumero;
};

Note algo incrível nela: o construtor recebe apenas um parâmetro!

Nossa, que incrível! ¬¬

Bom, o que eu quero mostrar com isso são as maneiras de iniciar este objeto. A mais tradicional é:

NumeroEstupido obj( 2 );

Mas, como possui apenas 1 parâmetro no construtor, ele também pode ser iniciado assim:


NumeroEstupido obj = 2;

Nossa, não sabia! Isso é d+!

Não, não é. Se você conviveu até hoje sem isso, pode conviver por mais tempo. A menos que você queira muito que a classe tenha este comportamento, você deve desabilitá-lo. E é para isso que serve o comando explicit.

Se você declarar o construtor da NumeroEstupido da seguinte forma:

explicit NumeroEstupido( int num ): mNumero( num ){}

A inicialização NumeroEstupido obj = 2; não funcionará. Via de regra, sempre que você declarar uma classe com um construtor que recebe apenas 1 parâmetro, você deve declará-lo como explicit.

Ora, mas por quê??

Essa inicialização esquisita é a causa de muitos problemas e comportamentos inesperados em códigos. Vou dar um exemplo simples. Imagine que você tem a seguinte função:

NumeroEstupido soma( const NumeroEstupido& num1, const NumeroEstupido& num2 )
{
   return NumeroEstupido( num1.GetNumero() + num2.GetNumero() );
}

E a chama da seguinte forma:

NumeroEstupido numSoma = soma( 200, 250 );

Basicamente, o compilador vai fazer:
num1 = 200
num2 = 250

E retornará a soma dos dois, fazendo com que numSoma valha 450. Até aí tudo bem. Você tem um código com 10 milhões de linhas rodando perfeitamente desta forma.

Até que um dia alguém resolve adicionar uma nova função ao código:

signed char soma( signed char num1, signed char num2 )
{
   return num1 + num2;
}

Nada demais, afinal C++ permite que você tenha funções com o mesmo nome e parâmetros diferentes.
O problema é que você roda o seu código agora, e na sua antiga chamada

NumeroEstupido numSoma = soma( 200, 250 );

numSoma passa a valer -62, e não mais 450! Como isso é possível?

É simples. Agora, quando você chama soma( 200, 250 ), o compilador vai chamar a função que recebe signed chars, e não mais a que recebe NumeroEstupido. Como esta função retorna um signed char, ocorre um overflow e o resultado é um undefined behavior.

Se o construtor de NumeroEstupido fosse explicit, esse problema seria evitado, pois no primeiro momento você seria obrigado a chamar a função soma definindo explicitamente os objetos, ou seja, algo do tipo:

soma( NumeroEstupido( 200 ), NumeroEstupido( 250 ) );

Assim, quando a função com signed char fosse criada, o comportamento da chamada com NumeroEstupido permaneceria o mesmo.

Consegue pensar em mais problemas que poderiam acontecer?

segunda-feira, 12 de setembro de 2011

Vocês praticam inspeção de código?

Se eu tivesse que dar uma palestra sobre qualidade de código e pudesse escolher apenas um tema, escolheria inspeção de código. Esta é uma das melhores técnicas para reduzir a quantidade de bugs no software, mas infelizmente é muito pouco utilizada pelas empresas. Isso porque parar um desenvolvedor por algumas horas para simplesmente “olhar o código de outro” parece uma perda de tempo muito grande para a maioria das empresas.
Se você também pensa assim, espero que tenha paciência de ler este post até o fim. Provavelmente mudará de ideia.
Para começar, vou colocar aqui dados de algumas empresas de porte razoável sobre o assunto:

  • Um defeito é encontrado a cada 4h de teste, contra 4,4 defeitos que são encontrados durante 1h de revisão (Hewlett-Packard);
  • Inspeções são 20x mais rápidas que testes (Russell);
  • 82% dos problemas são resolvidos antes de testar (IBM);
  • 1 defeito é encontrado a cada 3 minutos de inspeção (Shell);
  • Custo de manutenção dos códigos inspecionados é 1/10 do custo dos códigos não-inspecionados (Imperial Chemical).

Interessante, não?
A seguinte tabela foi extraída com base nos livros Software Defect-Removal Efficiency (Jones, 1996) e What we have learned about Fighting Defects (Shull 2002). Ela indica qual é, em média, a porcentagem de defeitos removidos utilizando cada uma das seguintes técnicas comuns de qualidade:

Técnica para encontrar defeitos
Qtde de Defeitos
Inspeções de design informais
35,00%
Inspeções de design formais
55,00%
Inspeções de código informais
25,00%
Inspeções de código formais
60,00%
Prototipação
65,00%
Unit-tests
30,00%
Testes de integração
35,00%
Testes de Regressão
25,00%
Testes do sistema
40,00%
Testes Beta (com menos de 10 usuários)
35,00%
Testes Beta (mais de mil usuários)
75,00%

Note que apenas a prototipação e os testes beta de alto volume encontram mais problemas do que a revisão de código.

Muitas empresas tentam aumentar a qualidade dos produtos aumentando a quantidade de testes do sistema. Isso é a mesma coisa que se pesar com mais frequência para ver se diminui o peso.
Não estou dizendo que os testes devem ser substituídos pelas revisões de código, mas sim que as duas técnicas devem ser utilizadas em conjunto.

Muito bonito na teoria, mas a minha equipe é pequena e não temos condições de parar desenvolvedores para fazer revisão de código!

Será?

Uma pesquisa mostra que a média de linhas de código desenvolvida por cada programador em um projeto de software é de 10 a 50 LOCs por dia. Isso parece absurdo em um primeiro momento, considerando que é possível escrever esta quantidade de linhas em poucos minutos, mas é preciso considerar também o tempo que os desenvolvedores perdem debugando, testando, tentando entender o código e corrigindo defeitos. Quantas vezes você já não passou um dia inteiro e não escreveu uma linha sequer?

O fato de você realizar uma inspeção de código irá diminuir a quantidade de defeitos no produto, aumentando a qualidade e diminuindo o tempo de manutenção. Assim, com esta prática, é possível entregar um produto de maior qualidade em um tempo igual ou menor de desenvolvimento.
Isso se prova com dados de campo: um estudo realizado pelo Laboratório de Engenharia de Software da NASA sobre 50 projetos indicou que, na média, os projetos com menos defeitos por linha de código tiveram um tempo de desenvolvimento menor do que os com maiores índices de defeito, porém com um custo igual (Code Complete, McConnell, 2004).

Outras opiniões?

  • A IBM revelou que cada hora investida em inspeção de código previne cerca de 100 horas de trabalho em testes e correções de defeitos;
  • Raytheon reduziu o custo de correções de defeitos de 40% do custo total do projeto para 20% através de uma iniciativa de inspeções (Haley 1996);
  • a HP reportou que seu programa de inspeção economiza em média 22 milhões de dólares por ano (Grady e Van Slack, 1994);
  • 11 programas foram desenvolvidos pela mesma equipe de desenvolvimento. 5 deles foram desenvolvidos sem utilizar revisão de código e tiveram uma média de 4,5 erros por 100 linhas de código. Os outros 6 foram inspecionados e e tiveram uma média de 0,82 erros por 100 linhas de código (Freedman e Weinberg, 1990).


Além disso, outra vantagem da revisão de código é a troca de conhecimento entre a equipe. Tanto quem desenvolveu quanto quem está revisando o código podem aprender bastante com o processo.

Mas que maravilha! E então, como é o processo de revisão de código?

Várias técnicas de revisão de código podem ser aplicadas. Atualmente, um dos modelos de desenvolvimento ágil mais conhecidos, o eXtreme Programming, utiliza o pair programming, que é um tipo de inspeção de código on-the-fly.

Vou explicar aqui um processo formal de revisão. Você pode adaptá-lo conforme a sua necessidade, porém não recomendaria grandes mudanças se sua empresa ainda não está habituada com a prática.


Processo de Revisão

Antes de explicar o processo, gostaria de ressaltar duas coisas:

  • Para sistemas simples, uma pessoa consegue revisar de 400 a 500 linhas de código por hora. Para sistemas críticos, complexos ou embarcados, esta média cai para cerca de 150 linhas por hora (nestas linhas não consideramos comentários ou linhas em branco);
  • Um revisor aguenta revisar um código por, no máximo, 2h seguidas.

Dito isso, vamos às pessoas envolvidas no processo:

Moderador: É o responsável por fazer o processo andar. Ele deve mover a revisão a uma velocidade que seja rápida o suficiente para ser produtiva, porém não tão rápida a ponto de que os defeitos não sejam encontrados. Também é o responsável por organizar as reuniões e definir quem deve revisar o quê.

Autor: A pessoa que desenvolveu o código. Participa das reuniões e deve ser o responsável por posteriormente aplicar as correções.

Revisor: É a pessoa que irá revisar o código (ou o design, se for o caso). Se for fazer revisão de código, deve ser um programador qualificado, de preferência que esteja envolvido com o processo, mas, obviamente, que NÃO seja o autor!

Escrituário (tá, foi a melhor tradução que eu encontrei para scribe!): pessoa responsável por escrever um relatório com os problemas encontrados durante a reunião. Esta pessoa pode ser o próprio revisor, mas é comum utilizar outra pessoa para liberar o revisor para suas outras atividades. É uma prática comum utilizar estagiários para este fim, com o intuito de que eles, desta forma, adquiram conhecimento e experiência (ok, as empresas utilizam estagiários porque são mais baratos do que os desenvolvedores, mas eu não queria chegar neste ponto...).

Não é comum que gerentes participem das revisões de código, pois isso pode constranger os outros participantes, principalmente se o gerente da equipe for um idiota (se você é gerente, você deve saber se a sua equipe te considera um idiota ou não). Os gerentes podem e devem acompanhar os resultados da revisão, mas não devem acompanhar o processo para que o foco da prática não passe de técnico para político.

As fases da revisão são:

Planejamento: O autor entrega o código (ou seja lá o que for ser revisado) para o moderador. Este decide quem deve realizar a revisão e quando. Dependendo da quantidade de trabalho, é até aconselhável que mais de um revisor seja convocado.
O moderador deve imprimir o código e reunir quaisquer outros documentos que considere importante.

Overview: Quando os revisores não são familiarizados com o projeto, é possível marcar uma reunião com os revisores e o autor, para que este dê uma visão geral do código que eles irão revisar. Esta reunião deve durar no máximo 1h, mas deve ser evitada se possível.

Preparação: Cada revisor trabalha sozinho eu seu código ou modelo que estiver revisano. É interessante que a empresa possua um checklist para facilitar o trabalho, ainda mais se a empresa segue um padrão de desenvolvimento.
Os revisores devem estar cientes das duas coisas que ressaltei no começo do tópico (quantidade de linhas para revisar por hora e tempo máximo que deve durar uma revisão).

Reunião de Inspeção: Nesta reunião, o moderador escolhe alguém (além do autor) para ler o código e explicá-lo. É interessante que o código esteja projetado na parede ou em um quadro, para que todos possam vê-lo. Assim que o código é passado, os erros encontrados são levantados e discutidos com a equipe. O escrituário é responsável por registrá-los. O moderador é responsável por manter a reunião produtiva e evitar que a equipe comece a discutir assuntos irrelevantes à revisão.
Esta reunião não deve durar mais do que duas horas.

Relatório de Inspeção: Um relatório com os problemas encontrados deve ser formalizado e enviado para todos os participantes. O relatório deve conter a lista dos defeitos e a severidade de cada um. Este relatório pode ser um documento de texto enviado para todos por e-mail ou um registro formal em algum bugtracker, não importa.

Retrabalho: O moderador distribui os defeitos encontrados para a equipe. Em geral o autor é responsável por corrigi-los, mas não necessariamente.

Follow-up: O moderador é responsável por acompanhar o andamento da correção dos defeitos encontrados. Dependendo da quantidade e da complexidade dos defeitos encontrados, um novo processo de revisão pode ser necessário na sequência.


É isso pessoal. Uma coisa que se deve ter em mente é que a revisão de código é apenas um processo de qualidade de software, assim como os testes do sistema, e NÃO deve ser utilizado para outros fins além deste. Um erro comum que leva o processo ao fracasso é utilizar o resultado das revisões para julgar ou humilhar o autor. Ou seja, dar um chapéu de burro porque alguém cometeu um erro besta é mais besta ainda.

Espero que tenha convencido vocês da importância deste processo, e boa revisão para todos!

quinta-feira, 1 de setembro de 2011

Vamos usar goto?


Meu Deus! Isso é coisa do capeta!

Muito se fala mal do bom e velho goto. Os professores abominam seu uso, o Java não tem (embora o use indiretamente por meio dos breaks) e a maioria dos livros mandam você manter distância desta função. Até mesmo o velho Dijkstra escreveu um artigo intitulado "Go To Statement Considered Harmful", então se ele falou está falado, não é?

Mas não é bem assim. O ódio ao goto pode ser explicado por um contexto histórico. O bom e velho Assembly não possui if/then/else, while, for ou coisa do tipo. O Fortran, nos seus primórdios, também não possuía. Todos os laços e decisões do código eram feitos por meio de go tos. Assim, os primeiros programadores de C estavam acostumados com gotos e os utilizavam sem dó. Isso é um convite para transformar seu código em um belo dum espaguete, impossível de ser compreendido.

Porém os tempos são outros. Usar gotos para decisões e laços é, sim, uma bela duma merda, mas existem situações em que o goto pode te ajudar a construir um código mais limpo.

Vamos ao exemplo:

Exemplo 1:


//Procurando Nemo
for ( i = 0; i < 1000; ++i )
{
    for ( j = 0; j < 1000; ++j )
    {
        for ( k = 0; k < 1000; ++k )
        {
            //faz milhares de coisas aqui
            if ( oceano[i][j][k] == NEMO )
            {
                //Encontrei! Quero cair fora daqui!
            }
        }
    }
}

Ok, você encontrou Nemo. E agora, como sair dessa?
Se colocar um break, somente o laço do k será cortado. O restante continuará sendo executado.
Outra solução seria marcar uma flag como nemoEncontrado = true dentro do if e verificar em cada for, mas não seria lá a solução mais bonita do mundo.
E por que não colocar um goto aqui?


//Procurando Nemo
for ( i = 0; i < 1000; ++i )
{
    for ( j = 0; j < 1000; ++j )
    {
        for ( k = 0; k < 1000; ++k )
        {
            //faz milhares de coisas aqui
            if ( oceano[i][j][k] == NEMO )
            {
                //Encontrei! Quero cair fora daqui!
                goto FimBuscaNemo;
            }
        }
    }
}
FimBuscaNemo:

Pronto, um bom uso do goto :)

Outro exemplo seria para funções com múltiplos retornos.
É uma boa prática que cada função possua apenas 1 ponto de retorno, mas às vezes isso acarreta em um monte de ifs encadeados. Mais uma vez o goto pode te ajudar:

Exemplo 2:


int func()
{
    AlocaUmMonteDeCoisaAqui();

    if ( abrirArquivo() == false )
    {
        DesalocaTudoQueAlocou();
        return ERRO_ABRIR_ARQUIVO;
    }

    if ( arquivoVazio() == true )
    {
        DesalocaTudoQueAlocou();
        return ERRO_ARQUIVO_VAZIO;
    }

    if ( checkSum() == false )
    {
        DesalocaTudoQueAlocou();
        return ERRO_CHECKSUM;
    }

    if ( tamanhoArquivo() < 100 )
    {
        DesalocaTudoQueAlocou();
        return ERRO_TAMANHO_INVALIDO;
    }

    //faz um monte de coisa

    DesalocaTudoQueAlocou();
    return BELEZA;
}

Este é um exemplo muito comum no dia a dia. A função aloca um monte de coisa no começo (poderia bloquear semáforos também) e para cada retorno precisa desalocar tudo. É também um erro muito comum um programador ir dar manutenção, colocar um novo return em algum ponto e esquecer de chamar a DesalocaTudoQueAlocou().

Uma função mais elegante e segura poderia ser escrita como:


int func()
{
    int retval = BELEZA;

    AlocaUmMonteDeCoisaAqui();

    if ( abrirArquivo() == false )
    {
        retval = ERRO_ABRIR_ARQUIVO;
        goto Fim_Func;
    }

    if ( arquivoVazio() == true )
    {
        retval = ERRO_ARQUIVO_VAZIO;
        goto Fim_Func;
    }

    if ( checkSum() == false )
    {
        retval = ERRO_CHECKSUM;
        goto Fim_Func;
    }

    if ( tamanhoArquivo() < 100 )
    {
        retval = ERRO_TAMANHO_INVALIDO;
        goto Fim_Func;
    }

    //faz um monte de coisa

Fim_Func:

    DesalocaTudoQueAlocou();
    return retval;
}

Aí está, outro uso saudável do goto!

Ou seja, não precisa ter medo da linguagem. Basta apenas usá-la corretamente.

Duas regras básicas devem ser seguidas quanto ao uso desta função:

1 – Só salte para linhas a frente da atual, e não para trás;
2 – Possua, no máximo, dois pontos de destino em cada função.

Com isso, você pode se divertir a vontade!
É isso!

P.s.: Como somos politicamente incorretos, não poderíamos deixar de falar do COMEFROM, que é basicamente o oposto do GOTO!
O COMEFROM surgiu como uma piada de Assembly:


Não possui nenhuma utilidade a não ser bagunçar seu código, mas se você quiser se exibir para os outros, aqui vai um port do COMEFROM para C:


#define _(A) goto A;

#define COME_FROM(A) A:

int main()
{
    int x = 0;
    int y = 0;
    COME_FROM( fim_loop );

    ++x;
    ++y;

    _( fim_loop )

    return 0;
}

Boa sorte! :)