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]