C# - Boas Práticas :  Aprendendo com maus exemplos - Aplicando padrões de projeto - III


 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 segunda parte do artigo refatoramos o código tornando-o mais legível, e, neste artigo vamos continuar aplicando as boas práticas usando os recursos dos padrões de projeto e dos conceitos do paradigma da orientação a objeto.

Veja como ficou o nosso código:

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

    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;
        }
    }

Melhoramos muito a legibilidade e assim tornamos o código mais fácil de entender e de manter, mas ele ainda precisa de ajustes para ficar mais robusto.

Apesar das melhorias esse código ainda apresenta os seguintes problemas:

Podemos resolver esses problemas aplicando alguns padrões de projeto como :

É isso que vamos fazer...

Recursos usados:

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

Aplicando padrões de projeto

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

O princípio da responsabilidade única(SRP) é um princípio fundamental no desenho de software que reza o seguinte :

"Deve existir um e somente UM MOTIVO para que uma classe mude"

Portanto, uma classe deve ser implementada tendo apenas um único objetivo, uma responsabilidade.

Quando uma classe possui mais que um motivo para ser alterada é porque, provavelmente ela esta fazendo mais coisas do que devia, ou seja, ela esta tendo mais de um objetivo.

No nosso exemplo a classe GerenciadorDeDescontos apresenta mais de uma responsabilidade.

Vamos alterar o código dessa classe de forma que a classe tenha somente uma responsabilidade : Aplicar o desconto.

    public class GerenciadorDeDescontos
    {
        private readonly ICalculoDescontoStatusContaFactory _descontoStatusConta;
        private readonly ICalculoDescontoFidelidade _descontoFidelidade;

        public GerenciadorDeDescontos(ICalculoDescontoStatusContaFactory descontoStatusConta, ICalculoDescontoFidelidade descontoFidelidade)
        {
            _descontoStatusConta = descontoStatusConta;
            _descontoFidelidade = descontoFidelidade;
        }

        public decimal AplicarDesconto(decimal preco, StatusDaConta statusDaConta, int tempoDeContaEmAnos)
        {

            decimal precoDepoisDoDesconto = 0;

            precoDepoisDoDesconto = _descontoStatusConta.GetCalculoDescontoStatusConta(statusDaConta).AplicarDesconto(preco);
            precoDepoisDoDesconto = _descontoFidelidade.AplicarDesconto(precoDepoisDoDesconto, tempoDeContaEmAnos);

            return precoDepoisDoDesconto;
        }
    }

Vamos entender o que foi feito:

1 - Definimos duas interfaces :

  1. ICalculoDescontoStatusContaFactory - Calcular o desconto com base no status da conta
  2. ICalculoDescontoFidelidade - Calcular o desconto com base na fidelidade

Uma das boas práticas do paradigma da orientação a objeto é 'Programe para uma interface e não para uma implementação".

Assim criamos duas interfaces que deverão ser implementadas por classes concretas para realizar os cálculos indicados.

No construtor da classe GerenciadorDeDescontos estamos aplicando o padrão de projeto da Injeção de Dependência, visto que estamos injetando no construtor da classe as instâncias do tipo de cada interface criada que serão usadas no método AplicarDesconto para calcular o preço depois do desconto, evitando assim uma dependência de criar instância de classe concretas.

O padrão Dependency Injection isola a implementação de um objeto da construção do objeto do qual ele depende.

A injeção de dependência (DI) nos trás os seguintes benefícios;

Podemos implementar a injeção de dependência das seguintes maneiras:

A seguir temos o código da interface ICalculoDescontoFidelidade :

    public interface ICalculoDescontoFidelidade
    {
        decimal AplicarDesconto(decimal preco, int tempoDeContaEmAnos);
    }

Na interface definimos o método AplicarDesconto() que deverá ser implementado pela classe CalculoDescontoFidelidade() cujo código é visto a seguir:

    public class CalculoDescontoFidelidade : ICalculoDescontoFidelidade
    {
        public decimal AplicarDesconto(decimal preco, int tempoDeContaEmAnos)
        {
            decimal descontoPorFidelidadePercentual = (tempoDeContaEmAnos > Constantes.DESCONTO_MAXIMO_POR_FIDELIDADE) ?
(decimal)Constantes.DESCONTO_MAXIMO_POR_FIDELIDADE / 100 : (decimal)tempoDeContaEmAnos / 100;
            return preco - (descontoPorFidelidadePercentual * preco);
        }
    }

Agora vamos ao código da interface ICalculoDescontoStatusContaFactory :

    public interface ICalculoDescontoStatusContaFactory
    {
        ICalculoDesconto GetCalculoDescontoStatusConta(StatusDaConta statusDaConta);
    }

Observe que esta interface define o método GetCalculoDescontoStatusConta() como sendo do tipo da interface ICalculoDesconto que iremos criar a seguir.

E da classe CalculoDescontoStatusContaFactory que implementa esta interface :

    public class CalculoDescontoStatusContaFactory : ICalculoDescontoStatusContaFactory
    {
        public ICalculoDesconto GetCalculoDescontoStatusConta(StatusDaConta statusDaConta)
        {

            ICalculoDesconto calcular;

            switch (statusDaConta)
            {
                case StatusDaConta.NaoRegistrado:
                    calcular = new ClienteNaoRegistradoDesconto();
                    break;
                case StatusDaConta.ClienteComum:
                    calcular = new ClienteComumDesconto();
                    break;
                case StatusDaConta.ClienteEspecial:
                    calcular = new ClienteEspecialDesconto();
                    break;
                case StatusDaConta.ClienteVIP:
                    calcular = new ClienteVIPDesconto();
                    break;
                default:
                    throw new NotImplementedException();
            }
            return calcular;
        }
    }

Nesta classe temos o padrão Factory que vai decidir qual algoritmo de desconto será usado.

A utilização do padrão Factory é útil quando você precisa criar objetos dinamicamente sem conhecer a classe de implementação, somente sua interface: o padrão factory estabelece uma forma de desenvolver objetos que são responsáveis pela criação de outros objetos. 

No código usado observe que estamos usando a instância da interface ICalculoDesconto() para realizar o cálculo do desconto para cada tipo de cliente através de classes concretas que deverão ser implementadas.

Abaixo vemos o código da interface ICalculoDesconto:

   public interface ICalculoDesconto
    {
        decimal AplicarDesconto(decimal preco);
    }

Como o algoritmo de desconto pode ser diferente para cada status de conta temos que usar diferentes estratégias para implementar cada um deles. Temos aqui uma boa oportunidade para aplicar o padrão de projeto Strategy.

Esquema UML do padrão Strategy :

Strategy - Interface comum para todas as classes (variações concretas) que definem os diversos comportamentos esperados;

ConcreteStrategy - Classes que implementam os algoritmos que devem atender a cada contexto;

Context - Classe onde os objetos ConcreteStrategy serão instanciados;

 

E para concluir, temos a seguir o código de cada uma das classes que deverão implementar essa interface para calcular o desconto para cada tipo de cliente:

    public class ClienteNaoRegistradoDesconto : ICalculoDesconto
    {
        public decimal AplicarDesconto(decimal preco)
        {
            return preco;
        }
    }
    public class ClienteComumDesconto : ICalculoDesconto
    {
        public decimal AplicarDesconto(decimal preco)
        {
            return preco - (Constantes.DESCONTO_CLIENTE_COMUM * preco);
        }
    }
    public class ClienteEspecialDesconto : ICalculoDesconto
    {
        public decimal AplicarDesconto(decimal preco)
        {
            return preco - (Constantes.DESCONTO_CLIENTE_ESPECIAL * preco);
        }
    }
    public class ClienteVIPDesconto : ICalculoDesconto
    {
        public decimal AplicarDesconto(decimal preco)
        {
            return preco - (Constantes.DESCONTO_CLIENTE_VIP * preco);
        }
    }

Dessa forma separamos as responsabilidades da nossa classe e facilitamos os testes e a manutenção aplicando os seguintes padrões de projeto e princípios:

Nota : No caso do princípio OCP - Open Closed - que afirma que  "Classes deverão ser abertas para extensão mas fechadas para modificação", se um novo status de conta de cliente for incluída, teremos que incluir uma nova classe concreta que implementa a interface ICalculoDesconto(), mas também teremos que alterar a classe CalculoDescontoStatusContaFactory o que viola o princípio. (O que indica que precisamos melhorar ainda mais o nosso código.)

A seguir temos o diagrama de classes do projeto exibindo as interfaces e classes usadas:

Para concluir vamos comparar como ficou o código final da classe que aplica o desconto:

    public class Class1
    {
        public decimal Calcular(decimal valor, int tipo, int anos)
        {
            decimal resultado = 0;
            decimal desc = (anos > 5) ? (decimal)5 / 100 : (decimal)anos / 100;
            if (tipo == 1)
            {
                resultado = valor;
            }
            else if (tipo == 2)
            {
                resultado = (valor - (0.1m * valor)) - desc * (valor - (0.1m * valor));
            }
            else if (tipo == 3)
            {
                resultado = (0.7m * valor) - desc * (0.7m * valor);
            }
            else if (tipo == 4)
            {
                resultado = (valor - (0.5m * valor)) - desc * (valor - (0.5m * valor));
            }
            return resultado;
        }
    }
 
    public class GerenciadorDeDescontos
    {
        private readonly ICalculoDescontoStatusContaFactory _descontoStatusConta;
        private readonly ICalculoDescontoFidelidade _descontoFidelidade;

        public GerenciadorDeDescontos(ICalculoDescontoStatusContaFactory descontoStatusConta, ICalculoDescontoFidelidade descontoFidelidade)
        {
            _descontoStatusConta = descontoStatusConta;
            _descontoFidelidade = descontoFidelidade;
        }

        public decimal AplicarDesconto(decimal preco, StatusDaConta statusDaConta, int tempoDeContaEmAnos)
        {

            decimal precoDepoisDoDesconto = 0;

            precoDepoisDoDesconto = _descontoStatusConta.GetCalculoDescontoStatusConta(statusDaConta).AplicarDesconto(preco);
            precoDepoisDoDesconto = _descontoFidelidade.AplicarDesconto(precoDepoisDoDesconto, tempoDeContaEmAnos);

            return precoDepoisDoDesconto;
        }
    }

Para concluir segue abaixo o diagrama de classes(no estilo clássico) gerado pela ferramenta NClass para o projeto:

Apresentamos assim, nesta série de artigos, um exemplo de código simples que 'cheirava mal' onde aplicamos as boas práticas de programação com o objetivo de torná-lo robusto, fácil de testar, de entender e de manter.

A mensagem que desejamos passar é que você deve desenvolver o seu código tendo em mente que ele deve estar apto para estar em um ambiente de produção por um longo tempo, que deverá sofrer alterações a cada mudança de requisito e que as alterações necessárias devem poder ser feitas de maneira fácil por qualquer outro programador.

Embora possamos melhorar ainda mais o nosso código (aplicando o princípio OCP - tarefa de casa) temos ao final um código melhor do que quando iniciamos.

Pegue o projeto completo aqui:  BoasPraticas_CSharp.zip

Porque há um só Deus, e um só Mediador entre Deus e os homens, Jesus Cristo homem.
1 Timóteo 2:5

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