Código Ruim

Category : Refatoração

Um código bom é como uma prosa bem redigida, exprime com clareza sua intenção e exerce boa influência nas alterações futuras. Passamos a maior parte do tempo de programação lendo e aprendendo códigos dos outros e quanto mais agradável esta experiência, melhores são nossos resultados.

Infelizmente a realidade é um pouco diferente…

Na Semana passada um novato da equipe de desenvolvimento Delphi pediu para que eu verificasse um trecho de código no módulo de folha de pagamentos, pois achou engraçado a lógica condicional que fazia a mesma coisa independendente do resultado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
procedure TForm1.ProcessarLog();
begin
if cdsEventos.FieldByName('PROCESSADO').AsString = 'P' then
    LogVer.Add('|'+Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString)+
               '|'+Alinhar(DIREITA,  6, '0', cdsEventos.FieldByName('EVENTO').AsString)+
               '|'+Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString)+
               '|'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',cdsEventos.FieldByName('QUANTIDADE').AsFloat))+
               '|'+Alinhar(DIREITA, 18, ' ', '')+'|')
else
    if cdsEventos.FieldByName('PROCESSADO').AsString = 'A' then
        LogAlte.Add('|'+Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString)+
                    '|'+Alinhar(DIREITA,  6, '0', cdsEventos.FieldByName('EVENTO').AsString)+
                    '|'+Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString)+
                    '|'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',cdsEventos.FieldByName('QUANTIDADE').AsFloat))+
                    '|'+Alinhar(DIREITA, 18, ' ', )+'|')
    else
        begin
        LogCanc.Add('|'+Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString)+
                    '|'+Alinhar(DIREITA,  6, '0', cdsEventos.FieldByName('EVENTO').AsString)+
                    '|'+Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString)+
                    '|'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',cdsEventos.FieldByName('QUANTIDADE').AsFloat))+
                    '|'+Alinhar(DIREITA, 18, ' ', '')+'|')
        end;
end;

Ao passar o olho rapidamente enxerguei a mesma coisa que ele, as condicionais não serviam para nada. Prestando um pouco mais de atenção percebemos que de acordo com a o valor do campo ‘PROCESSADO’ a informação seria escrita num arquivo de log diferente (LogVer, LogAlte ou LogCanc).

A duplicação do código de escrita no log é tão gritante que oculta tudo mais que está em volta, escondendo sua intenção. Para piorar este trecho de código estava num procedimento que não tinha nada a ver com log de informações e fazia mais uma dúzia de coisas diferentes.

Refatorando

Apenas eliminando a duplicação a legibilidade melhorou drasticamente, e pudemos enxergar a intenção do código sem distrações. Com três refatorações simples melhoramos muito aquele trecho de código:

  • Movemos o código duplicado para uma variável que descreve exatamente o que aquele emaranhado de cocatenação de strings significa.
  • Renomeamos as variáveis que representam nossos objetos de log para algo mais descritivo.
  • Substituímos as strings mágicas ‘P’ e ‘A’ por constantes que expressam o significado oculto nestas strings.

Eis o resultado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
procedure TForm1.ProcessarLog();
begin
NovaEntrada := '|' + Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString) +
               '|' + Alinhar(DIREITA,  6, '0', cdsEventos.FieldByName('EVENTO').AsString) +
               '|' + Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString) +
               '|' + Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',cdsEventos.FieldByName('QUANTIDADE').AsFloat)) +
               '|' + Alinhar(DIREITA, 18, ' ', '') + '|');

if cdsEventos.FieldByName('Processado').AsString = PROCESSADO then
    LogProcessados.Add(NovaEntrada)
else if cdsEventos.FieldByName('Processado').AsString = ALTERADO then
    LogAlterados.Add(NovaEntrada)
else
    LogCancelados.Add(NovaEntrada);
end;

A identação do código também foi corrigida.

Com isso ganhamos clareza de intenção e o próximo novato que passar por este código irá entender o que está acontecendo ali sem distrações. Refatorações posteriores melhorarão ainda sua estrutura, pois ainda temos aquela dúzia de coisas misturadas no procedimento.

Mais melhorias

O código de formatação da entrada de log deve ser extraído para sua própria função. Com isso temos menos um desvio mental durante a leitura, já que dificilmente estariamos interessados em saber como a linha do log está sendo formatada aqui.

Se um dia a formatação fosse alterada teríamos que descobrir onde este código está e encontrá-lo em ‘ProcRegistrosFuncionarios()’, com proximadamente 200 linhas não seria nada intuitivo.

Extraímos o código de formatação para sua própria função:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
procedure TForm1.ProcessarLog();
begin
if cdsEventos.FieldByName('Processado').AsString = PROCESSADO then
    LogProcessados.Add(NovaEntradaLogFormatada())
else if cdsEventos.FieldByName('Processado').AsString = ALTERADO then
    LogAlterados.Add(NovaEntradaLogFormatada())
else
    LogCancelados.Add(NovaEntradaLogFormatada());
end;

function TForm1.NovaEntradaLogFormatada(): String
begin
    Result := '|' + Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString) +
              '|' + Alinhar(DIREITA,  6, '0', cdsEventos.FieldByName('EVENTO').AsString) +
              '|' + Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString) +
              '|' + Alinhar(DIREITA, 10, '0', FormatFloat('#0.00', cdsEventos.FieldByName('QUANTIDADE').AsFloat)) +
              '|' + Alinhar(DIREITA, 18, ' ', '') + '|');
end;

Plantando uma semente

O código existente influencia a maneira como os novos códigos são escritos. É como plantar uma semente, bom código irá gerar códigos cada vez melhores. Código ruim é como uma erva daninha que irá se multiplicar e acabar com seu sistema.

A refatoração nos deu mais clareza e disponibilizou uma função única para formatação da entrada no log. Imagina se fosse necessário incluir a opção para escrever o log em XML. Com a primeira versão do código provavelmente um programador inexperiente iria aumentar as duplicações e piorar mais o código que já estava ruim, criando uma aberração como:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
procedure TForm1.ProcessarLog();
begin
if FormatoXML then
begin
    if CDSGrid.FieldByName('PROCESSADO').AsString = 'P' then
        LogVer.Add('<funcionario="'+Alinhar(DIREITA, 11, '0', CDSGrid.FieldByName('FUNCIONARIO').AsString)+'"'
              ' evento="'+Alinhar(DIREITA,  6, '0', CDSGrid.FieldByName('EVENTO').AsString)+'"'
              ' ano_mes="'+Alinhar(DIREITA, 10, ' ', CDSGrid.FieldByName('ANO_MES').AsString)+'"'
              ' quantidade="'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',CDSGrid.FieldByName('QUANTIDADE').AsFloat))+'" />')
else if CDSGrid.FieldByName('PROCESSADO').AsString = 'A' then
    LogAlte.Add('<funcionario="'+Alinhar(DIREITA, 11, '0', CDSGrid.FieldByName('FUNCIONARIO').AsString)+'"'
              ' evento="'+Alinhar(DIREITA,  6, '0', CDSGrid.FieldByName('EVENTO').AsString)+'"'
              ' ano_mes="'+Alinhar(DIREITA, 10, ' ', CDSGrid.FieldByName('ANO_MES').AsString)+'"'
              ' quantidade="'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',CDSGrid.FieldByName('QUANTIDADE').AsFloat))+'" />')
else
    LogCanc.Add('<funcionario="'+Alinhar(DIREITA, 11, '0', CDSGrid.FieldByName('FUNCIONARIO').AsString)+'"'
              ' evento="'+Alinhar(DIREITA,  6, '0', CDSGrid.FieldByName('EVENTO').AsString)+'"'
              ' ano_mes="'+Alinhar(DIREITA, 10, ' ', CDSGrid.FieldByName('ANO_MES').AsString)+'"'
              ' quantidade="'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',CDSGrid.FieldByName('QUANTIDADE').AsFloat))+'" />')
end
else
begin
if cdsEventos.FieldByName('PROCESSADO').AsString = 'P' then
    LogVer.Add('|'+Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString)+
               '|'+Alinhar(DIREITA,  6, '0', cdsEventos.FieldByName('EVENTO').AsString)+
               '|'+Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString)+
               '|'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',cdsEventos.FieldByName('QUANTIDADE').AsFloat))+
               '|'+Alinhar(DIREITA, 18, ' ', '')+'|')
else
    if cdsEventos.FieldByName('PROCESSADO').AsString = 'A' then
        LogAlte.Add('|'+Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString)+
                    '|'+Alinhar(DIREITA,  6, '0', cdsEventos.FieldByName('EVENTO').AsString)+
                    '|'+Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString)+
                    '|'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',cdsEventos.FieldByName('QUANTIDADE').AsFloat))+
                    '|'+Alinhar(DIREITA, 18, ' ', )+'|')
    else
        begin
        LogCanc.Add('|'+Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString)+
                    '|'+Alinhar(DIREITA,  6, '0', cdsEventos.FieldByName('EVENTO').AsString)+
                    '|'+Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString)+
                    '|'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',cdsEventos.FieldByName('QUANTIDADE').AsFloat))+
                    '|'+Alinhar(DIREITA, 18, ' ', '')+'|')
        end;
end;
end;

Enquanto que com nossa nova versão refatorada, com a metade de linhas o resultado da alteração seria:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
procedure TForm1.ProcessarLog();
begin
if cdsEventos.FieldByName('Processado').AsString = PROCESSADO then
    LogProcessados.Add(NovaEntradaLogFormatada())
else if cdsEventos.FieldByName('Processado').AsString = ALTERADO then
    LogAlterados.Add(NovaEntradaLogFormatada())
else
    LogCancelados.Add(NovaEntradaLogFormatada());
end;

function TForm1.NovaEntradaLogFormatada(): String
begin
if FormatoXML then
    Result := '<funcionario="'+Alinhar(DIREITA, 11, '0', CDSGrid.FieldByName('FUNCIONARIO').AsString)+'"'
              ' evento="'+Alinhar(DIREITA,  6, '0', CDSGrid.FieldByName('EVENTO').AsString)+'"'
              ' ano_mes="'+Alinhar(DIREITA, 10, ' ', CDSGrid.FieldByName('ANO_MES').AsString)+'"'
              ' quantidade="'+Alinhar(DIREITA, 10, '0', FormatFloat('#0.00',CDSGrid.FieldByName('QUANTIDADE').AsFloat))+'" />'
else
    Result := '|' + Alinhar(DIREITA, 11, '0', cdsEventos.FieldByName('FUNCIONARIO').AsString) +
              '|' + Alinhar(DIREITA, 6, '0', cdsEventos.FieldByName('EVENTO').AsString) +
              '|' + Alinhar(DIREITA, 10, ' ', cdsEventos.FieldByName('ANO_MES').AsString) +
              '|' + Alinhar(DIREITA, 10, '0', FormatFloat('#0.00', cdsEventos.FieldByName('QUANTIDADE').AsFloat)) +
              '|' + Alinhar(DIREITA, 18, ' ', '') + '|');
end;

Ainda não está perfeito, mas podemos ir refatorando cada vez mais, como extrair as responsabilidades de log para outra classe, e de lá aplicar padrões de projeto como o Strategy, aumentando ainda mais a reusabilidade do código. Eu parei por aqui porque precisamos entregar logo a correção efetuada no projeto.

Conclusão

Código legado largado é difícil de entender e alterar. Sua manutenção se torna penosa para o programador e consequentemente encarece o software, já que fatores como tempo e qualidade entram em jogo.

Escreva seu código para que seus leitores o entenda sem precisar chamar o colega ao lado e influencie-os a dar bons passos nas alterações futuras. É sua responsabilidade como programador.

Como disse Martin Fowler em seu livro Refactoring: Improving the Design of Existing Code:

“Qualquer tolo consegue escrever código que um computador entenda. Somente bons programadores escrevem código que humanos possam entender.”

Aprenda a expressar a intenção do seu código, não seja um tolo.

Livros recomendados

Clean Code: A Handbook of Agile Software Craftsmanship [Robert C. Martin]
Refactoring: Improving the Design of Existing Code [Martin Fowler]

Desenvolvimento Orientado por Testes

Category : TDD

Como comentei no meu primeiro artigo aqui no Blog, durante meus estudos para a SCJP conheci muitas coisas bacanas. Foi meu primeiro contato com algumas das metodologias do Agile. Entre elas a que mais procurei praticar, mesmo enquanto criava simples códigos de aprendizagem era o Test Driven Development (TDD).

A premissa do TDD é que o desenvolvedor deve escrever o código de testes antes de escrever o código de produção. Minha primeira reação ao ler isso era de achar maluquice: “como posso testar algo antes mesmo de existir?”. A resposta para isso encontrei pesquisando mais sobre o assunto e tudo passou a fazer muito sentido depois que entendi seus conceitos. Meu pensamento então passou a ser: “como um programador pode viver sem isso?”.

As três leis básicas

Para começar você deve seguir três regras básicas:

  • Escreva um teste que falhe.
  • Não escreva mais de um teste que falhe, sendo que não compilar é falhar.
  • Escreva o código de produção necessário para passar neste teste.

Um exemplo simples

Você começa a escrever um código de teste que propositalmente falhe, já que neste ponto não deverá haver nenhum código de produção. No JUnit 4 o caso de teste seria algo simples como:

1
2
3
4
5
6
7
8
9
10
import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class HelloWorldTest {
    @Test
    public void aSimpleHelloTest() {
        HelloWorld hello = new HelloWorld();
        assertEquals("Hello world!", hello.sayHello());
    }
}

Execute o teste. O mesmo deverá falhar neste ponto.

Em seguida implemente na classe alvo o código de produção de maneira que passe no teste. Por exemplo:

1
2
3
4
5
public class HelloWorld {
    public String sayHello() {
        return "Hello world";
    }
}

Execute o teste novamente e faça com que o código passe no teste. Repita o processo.

“No Eclipse você pode facilmente criar uma unidade de testes indo em New : Other : Junit Test Case. Certifique-se de utilizar o JUnit 4 para seus testes. Para executá-lo bas ir em Run : Run As : JUnit Test

Benefícios

Gosto de classificar cinco benefícios quando me perguntam sobre TDD.

1. Entendimento do domínio:

Escrevendo testes antes do código o desenvolvedor é levado a refletir sobre o que está sendo implementado, o que leva a uma maior clareza sobre o domínio do problema, e até mesmo a levantar fatos que poderiam passar despercebido na especificação de requisitos.

2. Segurança:

Os testes são um selo de garantia que tudo está funcionando corretamente. Se for necessário alterar algo no futuro, o desenvolvedor terá certeza que a alteração não quebrou nada.

3. Ganho de produtividade:

A codificação de um caso teste é simples e ocupa uma fração insignificante do tempo de desenvolvimento. A unidade de testes se transforma numa documentação sempre atualizada do código. Com isso em mãos o desenvolvedor fará a manutenção do código mais rápidamente.

4. Melhoria na qualidade:

Os testes de unidade garantem que o que foi implementado funcione sempre da maneira esperada. Muitos dos bugs que poderiam passar despercebidos em um código sem teste não passarão aqui. O desenvolvedor que reflete sobre o domínio sendo implementado cria códigos menos propensos a bugs.

5. Melhoria estrutual do código:

Desenvolver orientado por testes nos força a criar códigos com menor acoplamento, orientado a objetos. Os testes também são essenciais para a refatoração do código. Como eu disse outro dia no Twitter: “Refatorar sem testes é como paraquedismo sem para-quedas!”

Links relacionados:

http://www.junit.org/
http://en.wikipedia.org/wiki/Test_driven_development
http://en.wikipedia.org/wiki/Agile_software_development

Livros relacionados

Clean Code: A Handbook of Agile Software Craftsmanship [Robert C. Martin]
Test-driven development: by example [Kent Beck]

Cadê o Meu Gato? Ou, Cadê o Bug?

Category : Boas Práticas

Kitty BugFaz tempo estava querendo escrever este post, mas um fato interessante me fez mudar o título e o texto.

Nosso novo bichano de estimação na primeira semana desapareceu do apartamento. Minha esposa me ligou desesperada dizendo que ele havia fugido enquanto eu saia pra trabalhar. Esta idéia já estava formada na cabeça dela, e não adiantou eu argumentar que sempre tomo cuidado ao sair e que a gata ainda filhote sempre fica insegura quando a porta é aberta e prefere se esconder a sair.

Depois de procura-la por todo o prédio, casas e lotes vizinhos minha esposa resolveu desistir e colocar cartazes de “procura-se” por aí. Eu tinha certeza que ela estava em casa, mas ela teimou que já havia revirado todos os locais e nenhum sinal do bichinho.

Você pode rir, mas ao chegar a noite em casa não demorei mais de cinco minutos para encontrar a danadinha. Fui no local mais óbvio, onde todo gato gosta de entrar quando não estamos olhando, o guarda-roupas.

Mas o que isso tem a ver com software?

Este fato me levou a lembrar um erro que cometi poucas semanas antes na implementação de uma classe responsável pela manipulação dos dados de produtos no sistema.

Eu conhecia a classe de cor e só precisava adicionar uma nova propriedade. Basicamente um par de getter/setter. O design da classe é um pouco mais complicado que isso, e não vem ao caso, colocar uma nova propriedade implica sempre em mais algumas gambiarras tarefas por uma decisão de design.

Mas eu estava confiante, já conhecia o código e estava mais preocupado em terminar outra tarefa de maior importância. Basicamente usei a clássica combinação rápida que destrói qualquer código: Ctrl+C, Ctrl+V e renomear. Agora era só voltar para a tarefa de maior importância e commitar aquele bug que eu acabara de criar.

Não existe menos ou mais atenção!

Ou você está prestando atenção no que está fazendo ou não. Pequenas alterações, por mais insignificantes que sejam precisam de testes. Testar apenas as partes mais complexas de um software é se submeter a perder horas procurando pela vizinhança por algo que está no seu guarda-roupas quando ocorrer um bug.

Leve a sério e pratique TDD e BDD ou você nunca vai ter certeza se seu gato fugiu ou está tirando uma soneca dentro de uma gaveta!