.NET - Usando padrões de projeto e princípios OOP na prática


 Neste artigo vou descrever como podemos usar alguns dos princípios básicos de arquitetura e padrões de projeto da orientação a objetos para escrever um código aderente às boas práticas de programação.

Eu tenho escrito diversos artigos sobre boas práticas de programação e sobre padrões de projeto relacionados ao paradigma da orientação a objetos onde os conceitos são apresentados com exemplos para ilustrar como o conceito funciona.

No entanto, apenas conhecer os princípios e padrões não é o suficiente. Você tem que aprender a usar os conceitos em sua dia a dia para tirar vantagem dos recursos e dos benefícios que eles trazem ao desenvolvimento de software. Não adianta nada saber de cor os padrões de projeto se você continua a escrever o seu código como se eles não existissem.

Nada melhor do que praticar para ganhar experiência e assim poder usar o que temos aprendido e aplicar os conceitos em nosso código.

O objetivo deste artigo é mostrar como, partindo de um código mal escrito, podemos aplicar alguns padrões de projetos e princípios OOP para melhorar o código tornado-o mais coeso e menos acoplado.

Queremos que o nosso código seja SÓLIDO e os padrões de projeto nos ajudarão a atingir esse objetivo.

Recursos usados:

Implementando padrões de projeto e princípios SOLID na prática

Abra o VS Community 2013 clique em New Project;

Selecione Other Project Type -> VIsual Studio Solution e o template Blank Solution e informe o nome Aplicando_Padroes_Principios_OOP e clique no botão OK;

Será criada uma solução vazia. Vamos incluir agora dois projetos nesta solução. Um projeto usando a linguagem C# e outro projeto usando a linguagem Visual Basic.

Agora no menu FILE clique em Add -> New Project;

Selecione a linguagem Visual Basic e o template Console Application e informe o nome Aplicando_Padroes_VBNET e clique no botão OK;

Novamente clique no menu FILE clique em Add -> New Project;

Selecione a linguagem Visual C# o template Console Application e informe o nome Aplicando_Padroes_CSharp e clique no botão OK;

O objetivo em criar dois projetos com duas linguagens diferentes é mostrar o código usado no artigo nas linguagens C# e VB .NET.

Vamos criar 3 classes em cada projeto :

Para criar uma classe selecione o projeto e no menu PROJECT clique Add Class e informe o nome da classe.

A seguir vemos o código de cada uma das classes na linguagem C# e na linguagem VB .NET :

using System.Collections.Generic;
using System.Linq;

namespace Aplicando_Padroes_CSharp
{
    public class Pedido
    {
        List<PedidoItem> _pedidoItens = new List<PedidoItem>();

        public decimal CalcularTotal(Cliente _cliente)
        {
            decimal total = _pedidoItens.Sum((item) =>
            {
                return item.Custo * item.Quantidade;
            });

            decimal imposto;
            if (_cliente.UF == "SP")
                imposto = total * .18m;
            else if (_cliente.UF == "RJ")
                imposto = total * .19m;
            else
                imposto = .13m;

            total = total + imposto;

            return total;
        }
    }
}

Public Class Pedido

    Private _pedidoItens As New List(Of PedidoItem)()

    Public Function CalcularTotal(_cliente As Cliente) As Decimal

        Dim total As Decimal = _pedidoItens.Sum(Function(item)
                                        Return item.Custo * item.Quantidade
                                        End Function)

        Dim imposto As Decimal
        If _cliente.UF = "SP" Then
            imposto = total * 0.18D
        ElseIf _cliente.UF = "RJ" Then
            imposto = total * 0.19D
        Else
            imposto = 0.13D
        End If
        total = total + imposto

        Return total
    End Function
End Class

 

namespace Aplicando_Padroes_CSharp
{
    public class PedidoItem
    {
        public decimal Custo { get; set; }
        public int Quantidade { get; set; }
        public string Codigo { get; set; }
    }
}
Public Class PedidoItem
    Public Property Custo() As Decimal
    Public Property Quantidade() As Integer
    Public Property Codigo() As String
End Class

 

namespace Aplicando_Padroes_CSharp
{
    public class Cliente
    {
        public string Nome { get; set; }
        public string Endereco { get; set; }
        public string Cep { get; set; }
        public string UF { get; set; }
        public string Cidade { get; set; }
        public string Email { get; set; }
    }
}
Public Class Cliente
    Public Property Nome() As String
    Public Property Endereco() As String
    Public Property Cep() As String
    Public Property UF() As String
    Public Property Cidade() as String
    Public Property Email() As String
End Class

 

Agora olhe bem para o código acima, analise, pense e responda : O que há de errado com esse código ?

Ele esta violando o primeiro princípio SOLID :  O princípio da responsabilidade única ou SRP.

Em outras palavras, na classe Pedido, o objeto pedido deveria ser responsável apenas por assuntos relacionados com o Pedido como calcular o total do custo dos pedidos. (Poderíamos considerar que essa tarefa também não fosse de responsabilidade da classe Pedido mas para não complicar muito o exemplo vamos deixar assim...)

Mas observe que o código também esta sendo responsável por realizar os cálculos do imposto por estado de origem do cliente tendo que verificar o estado, aplicar alíquotas e calcular o imposto.

Esse código também esta violando o princípio Aberto/Fechado ou Open/Closed.

Por que ?

Porque esse princípio diz que uma vez que uma classe seja definida ela deve estar fechada para alteração mas aberta para ser estendida(via herança).

Segundo esse princípio uma classe após ser criada e testada não pode ter o seu código alterado.

Na classe Pedido, o código usado para calcular o imposto, utiliza o código do estado e sua respectiva alíquota para calcular o imposto, e, não é difícil perceber que qualquer alteração nas alíquotas do imposto fará com que o código da classe Pedido tenha que ser alterado.

Temos então neste pequeno trecho de código violações dos dois primeiros princípios SOLID : SRP e Open/Close.

Vamos alterar o código aplicando padrões...

Vamos aplicar o padrão Strategy.

"O padrão Strategy define uma família de algoritmos intercambiáveis de forma que estes sejam independentes dos clientes que os utilizam. "

Resumindo:

   Objetivo: Encapsular um algoritmo em um objeto.
                 Fornecer interfaces genéricas o suficiente para suportar uma variedade de algoritmos.
                 Facilitar a escolha e troca (intercâmbio) de algoritmos criados com uma mesma função.

O pulo do gato para usar o padrão Strategy é perceber o que pode mudar no seu código e encapsular.

Vamos encapsular o cálculo do imposto pois o código usado para fazer isso pode mudar.

Para isso vamos criar uma classe Imposto e mover o cálculo do imposto da classe Pedido para essa classe. Veja como ficou o código:

using System.Collections.Generic;
using System.Linq;

namespace Aplicando_Padroes_CSharp
{
    public class Pedido
    {
      List<PedidoItem> _pedidoItens = new List<PedidoItem>();

     public decimal CalcularTotal(Cliente _cliente)
     {
        decimal total = _pedidoItens.Sum((item) =>
        {
            return item.Custo * item.Quantidade;
        });

      total = total + new Imposto().CalcularImposto(_cliente,total);
      return total;
   }
 }
}

Public Class Pedido

 Private _pedidoItens As New List(Of PedidoItem)()

 Public Function CalcularTotal(_cliente As Cliente) As Decimal

       Dim total As Decimal = _pedidoItens.Sum(Function(item)
                                   Return item.Custo * item.Quantidade
 End Function)
    

 total = total + total = total + New Imposto().CalcularImposto(_cliente, total)

        Return total
    End Function
End Class

 

namespace Aplicando_Padroes_CSharp
{
    public class Imposto
    {
        public decimal CalcularImposto(Cliente _cliente, decimal total)
        {
            decimal imposto;
            if (_cliente.UF == "SP")
                imposto = total * .18m;
            else if (_cliente.UF == "RJ")
                imposto = total * .19m;
            else
                imposto = .13m;
            return imposto;
        }
    }
}
Public Class Imposto

    Public Function CalcularImposto(_cliente As Cliente,
total As Decimal) As Decimal
        Dim imposto As Decimal
        If _cliente.UF = "SP" Then
            imposto = total * 0.18D
        ElseIf _cliente.UF = "RJ" Then
            imposto = total * 0.19D
        Else
            imposto = 0.13D
        End If
        Return imposto
    End Function

End Class

 

Agora o objeto Pedido não é mais responsável pela lógica do cálculo do imposto. A classe Imposto agora é que cuida disso.  Este é um exemplo simples de aplicação do padrão Strategy.

Resolvemos um problema mas estamos violando outro princípio SOLID: o princípio Dependency Inversion ou princípio da Inversão da Dependência.

Este princípio diz que  as classes devem ser dependentes de abstrações e não de implementações concretas.

Uma abstração na linguagem C# e VB .NET é representada por uma interface ou uma classe abstrata.

Note que no nosso código cima temos um dependência de uma implementação concreta da classe Imposto e fazemos isso criando uma instância dessa classe na classe Pedido : New Imposto().

Nota : E ainda temos a classe Imposto() violando o princípio Open/Closed desde que o seu código terá que ser alterado quando uma alíquota mudar.

Então vamos continuar, criando uma interface IImposto() para abstrair a implementação da classe Imposto. O código agora ficou assim:

namespace Aplicando_Padroes_CSharp
{
    public interface IImposto
    {
        decimal CalcularImposto(Cliente _cliente, decimal total);
    }
}
Public Interface IImposto

    Function CalcularImposto(cliente As Cliente, total
As Decimal) As Decimal

End Interface

 

namespace Aplicando_Padroes_CSharp
{
    public class Imposto : IImposto
    {
        public decimal CalcularImposto(Cliente _cliente, decimal total)
        {
            decimal imposto;
            if (_cliente.UF == "SP")
                imposto = total * .18m;
            else if (_cliente.UF == "RJ")
                imposto = total * .19m;
            else
                imposto = .13m;
            return imposto;
        }
    }
}

 

Public Class Imposto
    Implements IImposto

    Public Function CalcularImposto(_cliente As Cliente, total
As Decimal) As Decimal Implements IImposto.CalcularImposto

        Dim imposto As Decimal
        If _cliente.UF = "SP" Then
            imposto = total * 0.18D
        ElseIf _cliente.UF = "RJ" Then
            imposto = total * 0.19D
        Else
            imposto = 0.13D
        End If
        Return imposto

    End Function

End Class

Melhoramos um pouco o código mas ainda estamos usando uma implementação concreta da classe Imposto() na classe Pedido, e  a classe Pedido não pode ser responsável por isso e não deve saber detalhes de implementação da classe Imposto.

Vamos usar outro padrão de projeto : o padrão Factory,  para criar objetos Imposto para a classe Pedido e assim remover da classe Pedido a instanciação da classe Imposto.

Vamos criar uma classe chamada ImpostoFactory() que será a responsável por criar uma instância da classe Imposto:

namespace Aplicando_Padroes_CSharp
{
    public class ImpostoFactory
    {
        public IImposto GetObjectImposto()
        {
            return new Imposto();
        }
    }
}
Public Class ImpostoFactory

    Public Function GetObjectImposto() As IImposto
        Return New Imposto()
    End Function

End Class

 

using System.Collections.Generic;
using System.Linq;

namespace Aplicando_Padroes_CSharp
{
    public class Pedido
    {
        List<PedidoItem> _pedidoItens = new List<PedidoItem>();

        public decimal CalcularTotal(Cliente _cliente)
        {
            decimal total = _pedidoItens.Sum((item) =>
            {
                return item.Custo * item.Quantidade;
            });

            IImposto imposto = new ImpostoFactory().GetObjectImposto();

            total = total + imposto.CalcularImposto(_cliente,total);
            return total;
        }
    }
}

Public Class Pedido

    Private _pedidoItens As New List(Of PedidoItem)()

   Public Function CalcularTotal(_cliente As Cliente) As Decimal

   Dim total As Decimal = _pedidoItens.Sum(Function(item)
    Return item.Custo * item.Quantidade

 End Function)

        Dim imposto As IImposto = New
ImpostoFactory().GetObjectImposto()

        total = total + total = total +
 imposto.CalcularImposto(_cliente, total)

        Return total
    End Function

End Class

O padrão factory determina que em qualquer cenário haverá uma classe cuja única responsabilidade é criar outras classes baseadas em algum critério de mudança.

No nosso caso a classe ImpostoFactory() é responsável por criar objetos Imposto e agora o objeto Pedido não sabe como estamos criando o objeto Imposto ou com qual implementação concreta de IImposto ele esta trabalhando.

Mas perceba que ainda continuamos com uma referência a uma implementação concreta na classe Pedido. Agora a classe Pedido esta criando uma instância da classe concreta ImpostoFactory().

Parece que estamos como o cachorro que corre atrás do rabo não é mesmo ?

Como podemos sair desse impasse ?

Usando um padrão de projeto : Dependency Injenction ou Injeção de dependência.

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:

Vamos então criar uma abstração para a classe ImpostoFactory() chamada IImpostoFactory(). O código ficou assim:

namespace Aplicando_Padroes_CSharp
{
    public interface IImposto
    {
        decimal CalcularImposto(Cliente _cliente, decimal total);
    }
}
Public Interface IImposto

    Function CalcularImposto(cliente As Cliente, total As Decimal)
As Decimal

End Interface

 

namespace Aplicando_Padroes_CSharp
{
    public interface IImpostoFactory
    {
        IImposto GetObjectImposto();
    }
}


  Public Interface IImpostoFactory
Function GetObjectImposto() As IImposto
  End Interface

 

namespace Aplicando_Padroes_CSharp
{
    public class ImpostoFactory : IImpostoFactory
    {
        public IImposto GetObjectImposto()
        {
            return new Imposto();
        }
    }
}
Public Class ImpostoFactory
    Implements IImpostoFactory

    Public Function GetObjectImposto() As IImposto Implements
 IImpostoFactory.GetObjectImposto
        Return New Imposto()
    End Function
End Class

 

using System.Collections.Generic;
using System.Linq;

namespace Aplicando_Padroes_CSharp
{
    public class Pedido
    {
        IImpostoFactory _impostoFactory;

        public Pedido(IImpostoFactory impostoFactory)
        {
            _impostoFactory = impostoFactory;
        }

        List<PedidoItem> _pedidoItens = new List<PedidoItem>();

        public decimal CalcularTotal(Cliente _cliente)
        {
            decimal total = _pedidoItens.Sum((item) =>
            {
                return item.Custo * item.Quantidade;
            });

            IImposto imposto = _impostoFactory.GetObjectImposto();
            total = total + imposto.CalcularImposto(_cliente,total);
            return total;
        }

    }
}




  Public Class Pedido

    Private _impostoFactory As IImpostoFactory

    Public Sub New(impostoFactory As IImpostoFactory)
        _impostoFactory = impostoFactory
    End Sub

    Private _pedidoItens As New List(Of PedidoItem)()

    Public Function CalcularTotal(_cliente As Cliente) As Decimal
        Dim total As Decimal = _pedidoItens.Sum(Function(item)
                                 Return item.Custo * item.Quantidade
                             End Function)

        Dim imposto As IImposto =
 _impostoFactory.GetObjectImposto()

        total = total + imposto.CalcularImposto(_cliente, total)
        Return total
 
   End Function


 End Class

Observe que criamos um construtor na classe Pedido que  usa o tipo IImpostoFactory e armazena uma referência a ele.

Essa referência é usada para chamar o método GetObjectImposto e retornar o imposto para em seguida calcular o imposto usando o método CalcularImposto().

Agora não temos nenhuma implementação concreta na classe Pedido  e ela não é mais responsável por criar objetos do tipo Imposto.

Usando a injeção de dependência conseguimos inverter o controle e agora estamos dependendo de abstrações e não de implementações concretas.

Poderíamos continuar a nossa diversão e aplicar outros padrões ao nosso código definindo novas regras de negócio mas creio que você já entendeu como podemos usar os padrões de projeto para melhorar o seu código.

Pegue o projeto completo aqui : Aplicando_Padroes_Principios_OOP.zip

De sorte que haja em vós o mesmo sentimento que houve também em Cristo Jesus,
Que, sendo em forma de Deus, não teve por usurpação ser igual a Deus,
Mas esvaziou-se a si mesmo, tomando a forma de servo, fazendo-se semelhante aos homens;
E, achado na forma de homem, humilhou-se a si mesmo, sendo obediente até à morte, e morte de cruz.

Filipenses 2:5-8

Referências:


José Carlos Macoratti