Boas Práticas de Programação


Para termos um código bem enxuto e com boa qualidade não basta apenas termos convenção de nomes, classes, variáveis, etc.. Precisamos ter um padrão para algumas situações para melhorarmos não somente a performance do código que está sendo escrito, mas também a qualidade de ordenação para que, futuros desenvolvedores, ao olharem o nosso código em uma manutenção, consigam facilmente identificar e assim trabalhar em cima do problema e não reaprender tudo o que fizeram, disperdiçando assim tempo e dinheiro.

Veremos no decorrrer desta seção algumas técnicas que não são muito utilizadas no dia-a-dia dos programadores pois são pequenos detalhes que influenciam na performance e na escrita de um bom código. Essas boas práticas vão desde a ter um código elegante até como melhorá-lo, mas claro, estaremos abordando isso superficialmente. Além do code-tunning falaremos um pouco sobre Exceptions.

Operadores de curto-circuito

Operadores que operam em curto-circuito é uma exclusividade de algumas linguagens de programação e, felizmente, entre elas estão o C# e o Visual Basic .NET. Esses operadores ajudam-nos a poupar verificações desnecessárias em nosso código, pois se algumas das condições falharem, a outra nem é executada.

Existem operadores AndAlso (&&) e OrElse (||) que operam em curto-circuito, os quais veremos alguns exemplos abaixo:

if(condicao1 && condicao2){ //… }

if(condicao1 || condicao2){ //… }

Como podemos analisar no código acima, os operadores de curto-circuito nos ajudam a ter uma melhor performance, já que no caso do operador AndAlso (&&), se a condição 1 falhar, a segunda condição não será avaliada. Já no caso do operador OrElse (||), se a primeira condição for verdadeira, a segunda também não será executada, pois o resultado já está formado.

Teste de ordenação lógica

Um detalhe bastante importante que muitas vezes não nos atentamos é quando utilizamos o bloco switch (Select Case em VB.NET). Neste caso, o ideal é sempre ordenarmos a lista de possibilidades da mais freqüente para a menos freqüente. Isso evitará que a avaliação seja feita em vários itens, tendo assim uma perda de performance, pois se o item freqüente está no último item a ser avaliado, ele deverá passar por todos os outros antes.

Para exemplificar faremos um teste em que vamos analisar o tempo que é levado para que o valor que está sendo procurado seja encontrado dentro da lista.

class Program
{
    private static void TesteCase(string value)
    {
        switch (value)
        {
            case "A":
                ProcessarValor(value);
                break;
            case "1":
                ProcessarValor(value);
                break;
            case "B":
                ProcessarValor(value);
                break;
            case "2":
                ProcessarValor(value);
                break;
            default:
                break;
        }
    }
}

Como vemos no código acima, se estivermos procurando pelo valor “2” dentro da lista de possibilidades do método TesteCase e, como ele é o último item e o número de pesquisa por ele for relativamente grande, então teríamos aqui uma pequena perda de performance. Para vermos o resultado veremos a média de tempo em 10 consultas com o valor “2” sendo o último item, e depois sendo o primeiro item da lista:

Último da Lista Primeiro da Lista
1 – 00:00:00.0005578 1 – 00:00:00.0005039
2 – 00:00:00.0005514 2 – 00:00:00.0004975
3 – 00:00:00.0005520 3 – 00:00:00.0004961
4 – 00:00:00.0005514 4 – 00:00:00.0004967
5 – 00:00:00.0005528 5 – 00:00:00.0004955
6 – 00:00:00.0005514 6 – 00:00:00.0004978
7 – 00:00:00.0005796 7 – 00:00:00.0004972
8 – 00:00:00.0005517 8 – 00:00:00.0004969
9 – 00:00:00.0005520 9 – 00:00:00.0004975
10 – 00:00:00.0005517 10 – 00:00:00.0004967

Fusão de Loops

A fusão de loops é quando usam-se dois loops distintos para operar o mesmo conjunto de elementos e, em cada um deles, efetuar uma ação diferente. Na maioria das vezes utilizamos isso em coleções para alterar algum valor, ou algo do tipo. Abaixo podemos visualizar o código que está com o problema e, em seguida, o código já melhorado:

for(int i = 0; i < dados.Count; i++)
{
    dados[i].Nome = string.Empty;
}

for(int i = 0; i < dados.Count; i++)
{
    dados[i].Id = -1;
}

A melhor opção para este código é:

for(int i = 0; i < dados.Count; i++)
{
    dados[i].Nome = string.Empty;
    dados[i].Id = -1;
}

Minimizando o trabalho dentro de Loops

Este é um dos pontos essenciais para ganharmos em performance na aplicação. Muitas vezes colocamos operações custosas dentro de loops, o que acarretará na execução desta operação o mesmo número de vezes que o loop for executado. Na maioria dos casos, esse código custoso faz sempre a mesma coisa, ou seja, é um cálculo que independe de qualquer valor proveniente do loop. Se analisarmos o código abaixo, veremos o problema:

for(int i = 0; i < dados.Count; i++)
{
    dados[i].Taxa = GeraTaxa() * 2.25;
}

Se executarmos o código acima, a função GeraTaxa multiplicada pelo valor 2.25 será executada o número de vezes que o loop acontecer. Como neste caso, o valor será sempre o mesmo, o ideal é você isolar o cálculo fora do loop e, conseqüentemente, ter um código mais performático:

double taxa = GeraTaxa() * 2.25;
for(int i = 0; i < dados.Count; i++)
{
    dados[i].Taxa = taxa;
}

Minimizando o acesso à Arrays

Um outro detalhe importante é a referência de arrays dentro de loops. Se for mal projetado você pode ter problemas de performance, já que você fará o acesso a algum índice de acordo com o número de iterações do teu loop. O exemplo de código abaixo, mostra essa deficiência:

for(int i = 0; i < dados.Count; i++)
{
    for(int j = 0; j < dados[i].Items.Count; j++)
    {
        dados[i].Items[j].Valor = dados[i].Valor + 2;
    }
}

A melhor opção para este código é mover o cálculo para fora do loop interno, já que o valor será proveniente do cálculo com um valor fornecido pelo loop principal. Sendo assim, o código fica da seguinte forma:

for(int i = 0; i < dados.Count; i++)
{
    double valor = dados[i].Valor;
    for(int j = 0; j < dados[i].Items.Count; j++)
    {
        dados[i].Items[j].Valor = valor + 2;
    }
}

Code Caching

Code caching significa salvar um determinado valor que é proveniente de algum cálculo mais complexo em um membro interno que, por sua vez, será exposto pela classe. Geralmente utilizamos essa técnica quando o valor é freqüentemente utilizado e, se for sempre calculado, teremos uma perda de performance, já que o cálculo seria efetuado o mesmo número de vezes que a propriedade é invocada.

Essa técnica exige que dentro da propriedade que expõe o valor “cacheado” devemos efetuar uma verificação para saber se o valor já foi ou não calculado. Para exemplificar, veremos abaixo um exemplo de como colocamos essa técnica em prática:

internal class Funcionario
{
    private double _salario;
    private bool _salarioGerado;

    public double Salario
    {
        get
        {
            if (!this._salarioGerado)
            {
                this._salario = CalcularSalario(this.FuncionarioId);
                this._salarioGerado = true;
            }
            return this._salario;
        }
    }
}

Como podemos ver, a primeira vez que a propriedade é chamada, a condicional verifica se o membro _salarioGerado é atendida, ou seja, verifica se o salário já foi ou não gerado. Se ainda não tiver sido gerado, o método CalcularSalario é executado, passando para o mesmo o identificador do funcionário para efetuar o cálculo do salário. O retorno deste método é armazenado no membro privado _salario, definindo o membro _salarioGerado para True. Sendo assim, da segunda vez que a propriedade é invocada pelo consumidor da classe, o cálculo, que é a parte mais custosa do código, não será mais executado devido ao flag que estará como True.

É natural que, em algum momento, você precise reiniciar o valor, pois algum processo dentro da classe necessite que o salário seja recalculado e, para isso, você pode simplesmente voltar o valor do membro privado _salarioGerado para False.

Magic Numbers

Os magic-numbers são aqueles números que temos no código que referenciam índices de arrays, campos de colunas do banco de dados, contadores, etc.. Em alguns casos, como por exemplo, o acesso aos campos do DataReader, a utilização de números para referenciar as colunas do result-set é sempre mais performático do que passar o nome do campo mais, muitas vezes, isso dificulta a legibilidade do código, principalmente se precisar dar manutenção neste código mais tarde.

Este é um cenário típico para o uso de variáveis constantes do tipo inteiro, que devemos especificar o nome do campo da base de dados e definir o número correspondente que este campo está dentro do result-set. Abaixo podemos ver um exemplo de como isso funciona:

#region Colunas da DB
const int ID = 0;
const int NOME = 1;
const int EMAIL = 2;
#endregion

Cliente c = new Cliente();
//....
c.Email = dr.GetString(EMAIL);
c.ID = dr.GetInt32(ID);
c.Nome = dr.GetString(NOME);

Isso não afetará em nada a performance da aplicação já que quando o código é compilado, o compilador se encarrega de trocar as constantes pelo valor correspondente.

Exceções

O foco desta seção não é abordar como é feito o tratamento de exceções no .NET, mas sim entender algumas das boas práticas e também algumas dicas com relação à performance.

Um ponto importante é que nem sempre uma exceção representa um erro. Exceção é uma violação de alguma suposição da interface do seu tipo. Por exemplo, ao projetar um determinado tipo, você imagina as mais diversas situações em que seu tipo será utilizado, definindo também seus campos, propriedades, métodos e eventos. Como já sabemos, a maneira como você define esses membros torna-se a interface do seu tipo.

Assim sendo, dado um método chamado TransferenciaValores, que recebe como parâmetro dois objetos do tipo Conta e um determinado valor (do tipo Double) que deve ser transferido entre elas, precisamos “validá-los” para que a transferência possa ser efetuada com êxito. O desenvolvedor da classe precisará ter conhecimento suficiente para implementar essa tal “validação” e não esquecer do mais importante: documentar claramente para que os utilizadores deste componente possam implementar o código que fará a chamada ao método da maneira mais eficiente possível, poupando ao máximo que surpresas ocorram em tempo de execução.

public static void TransferenciaValores(Conta de, Conta para, double valor)
{
    //....
}

Dentro deste nosso cenário, vamos analisar algumas suposições (“validações”) que devemos fazer para que o método acima possa ser executado da forma esperada:

  • Certificar que de e para não são nulos;
  • Certificar que de e para não referenciam a mesma conta;
  • Se o valor for maior que zero;
  • Se o valor é maior que o saldo disponível.

Necessitamos agora informar ao chamador que alguma dessas regras foi violada. Mas como fazemos isso? Atirando uma exceção. Como dissemos logo no começo desta seção, ter uma exceção nem sempre é algo negativo na aplicação, pois o tratamento de exceções permite capturar a exceção, tratá-la e a aplicação continuará correndo normalmente.

Capturando e atirando Exceções

Um erro bastante comum é utilizar blocos catch em demasia. Ao capturar uma exceção, você informa ao runtime o que espera por aquela exceção, entendendo o porque aquilo aconteceu, definindo uma diretiva para a aplicação.

try
{
    //código que pode eventualmente
    //atirar uma exceção
}
catch(Exception)
{
    //...
}

Como pode notar, o código acima espera que ocorra qualquer tipo de exceção para que ele o trate. Qualquer tipo que faça parte de uma biblioteca de classes, nunca deve capturar a exceção mais genérica, pois não há maneira da aplicação chamadora ser notificada de que algum erro ocorreu.

Se o código envolvido pelo código try lançar uma exceção, a mesma deverá ser lançada até o topo da pilha de chamadas e deixar o nível mais alto tratar a exceção da maneira que desejar.

Quando falamos em lançar a exceção devemos nos atentar em como realizar essa tarefa. Isso é feito através da keyword throw, onde você deve desenhar a sua estrutura sem especificar nenhuma exceção no bloco catch, ou seja, fazer todo o trabalho que necessita, ou melhor, voltar o objeto utilizado em um estado consistente e, finalmente, notificar o chamador que uma exceção aconteceu, atirando-a, sendo ela qual for.

try
{
    obj.Validate();
}
catch
{
    obj.ResetValues();
    throw;
}

Validações

Evite sempre o bloco de tratamento de exceções Try/Catch para fazer validações simples, como por exemplo cálculos, conversões, etc.. Há sempre uma alternativa mais performática e ao mesmo tempo mais elegante de fazer essas operações para evitar todo o overhead que existe em um bloco Try/Catch. Para exemplifcar isso, podemos citar alguns exemplos de códigos onde teremos primeiramente o código ruim e, em seguida, o mesmo código já reformulado:

Código ruim

try
{
    IReader reader = (IReader)e.Data;
    reader.ExecuteOperation();
}
catch (InvalidCastException)
{
    Response.Write("Tipo incompatível.");
}

try
{
    int id = Convert.ToInt32(Request.QueryString["Id"]);
    this.BindForm(id);
}
catch (InvalidCastException)
{
    Response.Write("Id inválido.");
}

Código reformulado

IReader reader = (IReader)e.Data as IReader;
if(reader != null)
{
    reader.ExecuteOperation();
}
else
{
    Response.Write("Tipo incompatível.");
}

int id = 0;
if(int.TryParse(Request.QueryString["Id"], out id))
{
    this.BindForm(id);
}
else
{
    Response.Write("Id inválido.");
}

Conclusão: Boas práticas de programação são sempre bem-vindas em qualquer tipo de linguagem. Claro que as técnicas não páram por aqui. Existem muitas outras técnicas e benefícios relacionados a cada uma delas que este artigo não contempla. Este artigo dá apenas uma visão de técnicas que devem ser utilizadas no desenvolvimento de aplicações baseadas na plataforma .NET para tirar um melhor proveito da linguagem, não perdendo performance.

Anúncios

Um comentário sobre “Boas Práticas de Programação

  1. Muito bom o post!

    É bompara abrir os olhos do pessoal para programar de forma otimizada. Já trabalhei muito com dispositivos móveis, e sei o quanto isso é fundamental.

    Sem querer deixar alguns programadores preguiçosos, mas no caso do "trabalho de loops", alguns compiladores, quando você ativa o processo de otimização de compilação, "melhora o código sozinho".

Deixe uma resposta

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