.NET

23 jul, 2015

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

Publicidade

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, no qual 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, informe o nome Aplicando_Padroes_Principios_OOP e clique no botão OK.

net-1

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

Agora, no menu FILE, clique em Add -> New Project.

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

net-2

Novamente, no menu FILE, clique em Add -> New Project.

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

net-3

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

Vamos criar três classes em cada projeto:

  • Cliente
  • Pedido
  • PedidoItem

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:

cod-1

cod-2

cod-3

C#:

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

namespace Aplicando_Padroes_CSharp
{
    public class PedidoItem
    {
        public decimal Custo { get; set; }
        public int Quantidade { get; set; }
        public string Codigo { get; set; }
    }
}

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

VB .NET:

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

Public Class PedidoItem

    Public Property Custo() As Decimal
    Public Property Quantidade() As Integer
    Public Property Codigo() As String

End Class

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 está 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 está 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 está violando o princípio Aberto/Fechado ou Open/Closed.

Por quê?

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 nesse 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:

cod-4

cod-5

C#:

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

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

VB .NET:

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

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 é quem cuida disso. Esse é 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.

Esse 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 acima 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:

cod-6

cod-7

C#

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

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

VB .NET:

Public Interface IImposto

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

End Interface

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:

cod-8

cod-9

C#:

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

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

VB .NET:

Public Class ImpostoFactory

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

End Class

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 está trabalhando.

Mas perceba que ainda continuamos com uma referência a uma implementação concreta na classe Pedido. Agora a classe Pedido está 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 traz os seguintes benefícios:

  • Elimina o forte acoplamento entre objetos;
  • Torna a aplicação e o objeto mais flexíveis;
  • Facilita a criação de objetos fracamente acoplados;
  • Facilita a criação de testes unitários.

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

  • Injeção via Construtor;
  • Injeção via Propriedades (get/set);
  • Injeção via Interface;
  • Injeção usando um framework(Spring/Unity);

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

cod-10

cod-11

cod-12

cod-13

C#:

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

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

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

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

VB .NET:

Public Interface IImposto

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

End Interface

    Public Interface IImpostoFactory
	Function GetObjectImposto() As IImposto
  End Interface

Public Class ImpostoFactory
    Implements IImpostoFactory

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




  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.