C# - Boas Práticas :  Aprendendo com maus exemplos - Refatorando o código - II


 Neste artigo vou mostrar como aplicar boas práticas de programação usando exemplos de código criados sem preocupação alguma em ter um código robusto, fácil de manter e fácil de entender. Vamos assim aprender a partir de maus exemplos de códigos.

Na primeira parte do artigo apresentamos o problema e agora vamos mostrar uma possível solução seguindo o caminho das boas práticas.

Vou realizar uma abordagem bem prática e simples mostrando que não é preciso muito esforço para produzir um código robusto.

Recursos usados:

Nota: Baixe e use a versão Community 2015 do VS ela é grátis e é equivalente a versão Professional.

Refatorando o código do projeto

Abra o projeto BoasPraticas_CSharp  criado no  VS Community 2015 no artigo anterior;

1 - Dando nome significativos às classes, variáveis, métodos, etc., 

A primeira coisa que vamos fazer é tornar o código mais legível usando nomes significativos em nossa classe.

Nota: Não existe um regra rígida que se deve seguir para nomear, mas existem recomendações que são seguidas. Você pode consultar as recomendações da Microsoft em : General Naming Conventions

Vamos começar dando um nome significativo à nossa classe que seja pertinente com a suas funcionalidades.

Eu não sou muito bom para dar nomes mas acho que um bom nome seria GerenciadorDeDescontos.

A seguir vamos usar o mesmo critério para variáveis, métodos, parâmetros.

Então. veja o ficou o código da nossa classe abaixo, e confira a diferença:

    public class GerenciadorDeDescontos
    {
        public decimal AplicarDesconto(decimal preco, int statusDaConta, int tempoDeContaEmAnos)
        {
            decimal precoDepoisDoDesconto = 0;
            decimal descontoPorFidelidadePercentual = (tempoDeContaEmAnos > 5) ? (decimal)5 / 100 : (decimal)tempoDeContaEmAnos / 100;
            if (statusDaConta == 1)
            {
                precoDepoisDoDesconto = preco;
            }
            else if (statusDaConta == 2)
            {
                precoDepoisDoDesconto = (preco - (0.1m * preco)) - descontoPorFidelidadePercentual * (preco - (0.1m * preco));
            }
            else if (statusDaConta == 3)
            {
                precoDepoisDoDesconto = (0.7m * preco) - descontoPorFidelidadePercentual * (0.7m * preco);
            }
            else if (statusDaConta == 4)
            {
                precoDepoisDoDesconto = (preco - (0.5m * preco)) - descontoPorFidelidadePercentual * (preco - (0.5m * preco));
            }
            return precoDepoisDoDesconto;
        }
    }

Vamos ser sincero, ficou muito mais fácil de entender o código agora.  E praticamente só tivemos o trabalho de dar nomes pertinentes a cada elemento do código.

Agora sabemos que o objetivo dessa classe GerenciadorDeDescontos é gerenciar descontos e que o método AplicarDesconto esta calculando o desconto a ser aplicado com base no preco, no status da conta do cliente e levando em conta o tempo de conta em anos que o cliente possui.

Bem,  esse foi o primeiro passo. Vamos continuar...

2 - Resolvendo o problema dos números mágicos

Uma técnica para evitar os famigerados números mágicos é usar o recurso da Enumeração.

O que é Enumeration ? (Enumerar)

Seria algo como nomear ou numerar um a um (geralmente em uma lista).

Assim, um Enumeration ou Enum é um nome simbólico para um conjunto de valores.

Na nossa classe podemos usar esse recurso para definir o status das contas e assim substituir os números mágicos nas instruções if-else if.

Podemos então definir uma enumeração da seguinte forma:

    public enum StatusDaConta
    {
        NaoRegistrado = 1,
        ClienteComum = 2,
        ClienteEspecial = 3,
        ClienteVIP = 4
    }

Aplicando a enumeração ao nosso código teremos o seguinte resultado:

    public enum StatusDaConta
    {
        NaoRegistrado = 1,
        ClienteComum = 2,
        ClienteEspecial = 3,
        ClienteVIP = 4
    }
    public class GerenciadorDeDescontos
    {
        public decimal AplicarDesconto(decimal preco, StatusDaConta statusDaConta, int tempoDeContaEmAnos)
        {
            decimal precoDepoisDoDesconto = 0;
            decimal descontoPorFidelidadePercentual = (tempoDeContaEmAnos > 5) ? (decimal)5 / 100 : (decimal)tempoDeContaEmAnos / 100;

            switch(statusDaConta)
            {
                case StatusDaConta.NaoRegistrado:
                    precoDepoisDoDesconto = preco;
                    break;
                case StatusDaConta.ClienteComum:
                    precoDepoisDoDesconto = (preco - (0.1m * preco)) - descontoPorFidelidadePercentual * (preco - (0.1m * preco));
                    break;
                case StatusDaConta.ClienteEspecial:
                    precoDepoisDoDesconto = (0.7m * preco) - descontoPorFidelidadePercentual * (0.7m * preco);
                    break;
                case StatusDaConta.ClienteVIP:
                    precoDepoisDoDesconto = (preco - (0.5m * preco)) - descontoPorFidelidadePercentual * (preco - (0.5m * preco));
                    break;
            }
            return precoDepoisDoDesconto;
        }
    }

Com isso aumentamos a legibilidade do código usando recurso da enumeração e substituímos as instruções if-else if pela instrução switch-case.

3- Melhorando a legibilidade do algoritmo de cálculo

Podemos melhorar ainda mais a legibilidade do código dividindo as linhas de código onde o cálculo do desconto esta sendo realizado.

Dessa forma ao invés de fazermos:

precoDepoisDoDesconto = (preco - (0.5m * preco)) - descontoPorFidelidadePercentual * (preco - (0.5m * preco));

Vamos dividir o cálculo em duas linhas :

precoDepoisDoDesconto = (preco - (0.5m * preco))
precoDepoisDoDesconto = precoDepoisDoDesconto - ( descontoPorFidelidadePercentual * precoDepoisDoDesconto );

Um detalhe simples mas que facilita o entendimento e a legibilidade do código.

Veja como ficou agora o nosso código:

    public class GerenciadorDeDescontos
    {
        public decimal AplicarDesconto(decimal preco, StatusDaConta statusDaConta, int tempoDeContaEmAnos)
        {
            decimal precoDepoisDoDesconto = 0;
            decimal descontoPorFidelidadePercentual = (tempoDeContaEmAnos > 5) ? (decimal)5 / 100 : (decimal)tempoDeContaEmAnos / 100;

            switch(statusDaConta)
            {
                case StatusDaConta.NaoRegistrado:
                    precoDepoisDoDesconto = preco;
                    break;
                case StatusDaConta.ClienteComum:
                    precoDepoisDoDesconto = (preco - (0.1m * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                case StatusDaConta.ClienteEspecial:
                    precoDepoisDoDesconto = (0.7m * preco);
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                case StatusDaConta.ClienteVIP:
                    precoDepoisDoDesconto = (preco - (0.5m * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
            }
            return precoDepoisDoDesconto;
        }
    }

Vamos aproveitar e alterar a forma de cálculo para o ClienteEspecial de  (0.7m*preco) para  preco - (0.3m*preco)

Dessa forma teremos agora o mesmo formato de cálculo para todos os clientes:

    public class GerenciadorDeDescontos
    {
        public decimal AplicarDesconto(decimal preco, StatusDaConta statusDaConta, int tempoDeContaEmAnos)
        {
            decimal precoDepoisDoDesconto = 0;
            decimal descontoPorFidelidadePercentual = (tempoDeContaEmAnos > 5) ? (decimal)5 / 100 : (decimal)tempoDeContaEmAnos / 100;

            switch(statusDaConta)
            {
                case StatusDaConta.NaoRegistrado:
                    precoDepoisDoDesconto = preco;
                    break;
                case StatusDaConta.ClienteComum:
                    precoDepoisDoDesconto = (preco - (0.1m * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                case StatusDaConta.ClienteEspecial:
                    precoDepoisDoDesconto = (preco - (0.3m * preco);
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                case StatusDaConta.ClienteVIP:
                    precoDepoisDoDesconto = (preco - (0.5m * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
            }
            return precoDepoisDoDesconto;
        }
    }

Temos agora um formato de cálculo uniforme que facilita a leitura do código.

4- Resolvendo o problema do bug oculto

Como já foi dito, esse código possui um bug que ocorre quando por algum motivo um novo status de conta for criado e atribuído a um cliente.

Neste cenário o código acima irá retornar o valor zero, que é o valor inicial atribuído à variável precoDepoisDoDesconto.

Para resolver esse bug temos que deixar explícito quando um valor de status de conta for recebido pela nossa classe, e, podemos fazer isso lançando uma exceção nestes casos.

Fazemos isso incluindo a seção default na instrução switch-case de forma que, se um valor para o status da conta que não estiver sendo tratado, uma exceção será lançada.

Veja como ficou o código da classe:

    public class GerenciadorDeDescontos
    {
        public decimal AplicarDesconto(decimal preco, StatusDaConta statusDaConta, int tempoDeContaEmAnos)
        {
            decimal precoDepoisDoDesconto = 0;
            decimal descontoPorFidelidadePercentual = (tempoDeContaEmAnos > 5) ? (decimal)5 / 100 : (decimal)tempoDeContaEmAnos / 100;

            switch(statusDaConta)
            {
                case StatusDaConta.NaoRegistrado:
                    precoDepoisDoDesconto = preco;
                    break;
                case StatusDaConta.ClienteComum:
                    precoDepoisDoDesconto = (preco - (0.1m * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                case StatusDaConta.ClienteEspecial:
                    precoDepoisDoDesconto = (preco - (0.3m * preco);
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                case StatusDaConta.ClienteVIP:
                    precoDepoisDoDesconto = (preco - (0.5m * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
               default:
                    throw new NotImplementedException();
            }
            return precoDepoisDoDesconto;
        }
    }

Resolvemos assim o problema do bug oculto no código evitando que um status de conta não esperado seja notificado como um alerta para correção.

5- Resolvendo o problema dos números mágicos novamente

Podemos melhorar ainda mais a legibilidade do nosso código dando um significado aos números 0.1, 0.7 e 0.5  que são usados no cálculo do desconto.

Temos que fazer a mesma coisa para o misterioso número 5 que ainda aparece no cálculo do desconto por fidelidade:

decimal descontoPorFidelidadePercentual = (tempoDeContaEmAnos > 5) ? (decimal)5 / 100 : (decimal)tempoDeContaEmAnos / 100;

E, para tornar mais descritivos esses números, podemos recorrer às constantes da linguagem C#.

Podemos usar a palavra-chave const para declarar um campo constante ou um local constante. Campos e locais constantes não são variáveis ​​e não podem ser modificados.

As constantes podem ser números, valores boleanos, cadeias de caracteres ou uma referência nula.

Vamos definir uma classe estática usando constantes para descrever melhor o significado desses números:

    public static class Constantes
    {
        public const int DESCONTO_MAXIMO_POR_FIDELIDADE = 5;
        public const decimal DESCONTO_CLIENTE_COMUM = 0.1m;
        public const decimal DESCONTO_CLIENTE_ESPECIAL = 0.3m;
        public const decimal DESCONTO_CLIENTE_VIP = 0.5m;
    }

Agora o nosso código ficou assim:

    public static class Constantes
    {
        public const int DESCONTO_MAXIMO_POR_FIDELIDADE = 5;
        public const decimal DESCONTO_CLIENTE_COMUM = 0.1m;
        public const decimal DESCONTO_CLIENTE_ESPECIAL = 0.3m;
        public const decimal DESCONTO_CLIENTE_VIP = 0.5m;
    }
    public class GerenciadorDeDescontos
    {
        public decimal AplicarDesconto(decimal preco, StatusDaConta statusDaConta, int tempoDeContaEmAnos)
        {
            decimal precoDepoisDoDesconto = 0;
            decimal descontoPorFidelidadePercentual = (tempoDeContaEmAnos > Constantes.DESCONTO_MAXIMO_POR_FIDELIDADE) ? 
(decimal)Constantes.DESCONTO_MAXIMO_POR_FIDELIDADE / 100 : (decimal)tempoDeContaEmAnos / 100;
            switch (statusDaConta)
            {
                case StatusDaConta.NaoRegistrado:
                    precoDepoisDoDesconto = preco;
                    break;
                case StatusDaConta.ClienteComum:
                    precoDepoisDoDesconto = (preco - (Constantes.DESCONTO_CLIENTE_COMUM * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                case StatusDaConta.ClienteEspecial:
                    precoDepoisDoDesconto = (preco - (Constantes.DESCONTO_CLIENTE_ESPECIAL * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                case StatusDaConta.ClienteVIP:
                    precoDepoisDoDesconto = (preco - (Constantes.DESCONTO_CLIENTE_VIP * preco));
                    precoDepoisDoDesconto = precoDepoisDoDesconto - (descontoPorFidelidadePercentual * precoDepoisDoDesconto);
                    break;
                default:
                    throw new NotImplementedException();
            }
            return precoDepoisDoDesconto;
        }
    }

Neste estágio tornamos o código da classe original mais legível e fácil de entender e também mais fácil de manter usando apenas regras simples de nomeação de classes, métodos, parâmetros e usando recursos básicos da linguagem C# como enumeração e constantes.

Podemos melhorar ainda mais o código aplicando os padrões de projeto para tornar o código mais robusto e vamos fazer isso na terceira parte do artigo.

Aguarde...

Deus nunca foi visto por alguém. O Filho unigênito (Jesus), que está no seio do Pai, esse o revelou.
João 1:18

Veja os Destaques e novidades do SUPER DVD Visual Basic (sempre atualizado) : clique e confira !

Quer migrar para o VB .NET ?

Quer aprender C# ??

Quer aprender os conceitos da Programação Orientada a objetos ?

Quer aprender o gerar relatórios com o ReportViewer no VS 2013 ?

Referências:


José Carlos Macoratti