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:
- Visual Studio Community 2013
- Padrões de Projetos
- Princípios SOLID
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.
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.
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.
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:
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:
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:
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:
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:
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.