Padrões de Projeto
Antes de iniciarmos um tema relativamente complexo e confuso para grande parte dos profissionais da engenharia de software, precisamos alertar sobre:
Segundo alguns profissionais experientes, excessos nas tentativas de fazer o código se conformar aos 'Padrões de Projeto' aumentam desnecessariamente a sua complexidade.
Na Engenharia de Software, um padrão projeto (ou do inglês design pattern) é uma solução geral para um problema que ocorre com frequência dentro de um determinado contexto no projeto de software.
Um padrão de projeto não é um projeto finalizado que pode ser diretamente transformado em código fonte ou de máquina, ele é uma descrição ou modelo (template) de como resolver um problema que pode ser usado em muitas situações diferentes.
Podemos afirmar que padrões são melhores práticas formalizadas que o programador possa usar para resolver problemas comuns quando projetar uma aplicação ou sistema?
Isso depende de alguns fatores:
- Escopo do projeto : Muitas das vezes o escopo do projeto é tão pequeno e objetivo que não se faz necessário;
- Maturidade do time : Não podemos pensar em adotar padrões de projetos se não há uma compreensão de conceitos e diretrizes por todos os envolvidos;
- Prazo e orçamento : Em alguns casos para estimativa de prazo e custo de desenvolvimento de um software não é inserido o valor e tempo de esforço em aplicar determinados padrões de projetos.
Após identificar a real necessidade e o impacto em adotar padrões de projetos, é necessário determinar as 04 principais características abaixo:
- Nome do padrão;
- Problema a ser resolvido;
- Solução dada pelo padrão;
- Consequências.
Exemplo: Vamos imaginar que fomos contratados para desenvolver um serviço de notificação de cobrança onde o cliente possa escolher se será notificado por e-mail ou sms.
Campo | Valor |
---|---|
Nome | Factory |
Problema | Determinar o tipo de notificação de cobrança de acordo com a preferência do cliente |
Solução | Criar uma fábrica de Serviços de notificação |
Consequência | A cada novo meio de notificação, deverá ser criada uma nova implementação |
Padrões são melhores práticas formalizadas que o programador pode usar para resolver problemas comuns quando projetar uma aplicação ou sistema. Algumas linguagens de programação adotam o padrão de projeto da programação orientada a objetos onde todo algoritmo é organizado por classes com atributos e métodos.
Padrões GoF
De acordo com o livro: "Padrões de Projeto: soluções reutilizáveis de software orientado a objetos", os padrões GoF ('Gang of Four') são divididos em 24 tipos. Em função dessa grande quantidade de padrões, foi necessário classificá-los de acordo com as suas finalidades.
São 3 as classificações/famílias:
- Padrões de criação: fornecem mecanismos de criação de objetos que aumentam a flexibilidade e a reutilização de código.
- Padrões estruturais: explicam como montar objetos e classes em estruturas maiores, enquanto ainda mantém as estruturas flexíveis e eficientes.
- Padrões comportamentais: cuidam da comunicação eficiente e da assinalação de responsabilidades entre objetos.
Abaixo temos uma ilustração dos padrões de projetos distribuídos em criacionais, estruturais e comportamentais.
Acreditamos que o maior desafio em implementar um padrão em nosso projeto é identificar um cenário e caso de uso apropriado, esta compreensão exige um certo tempo de interação e entendimento do problema apresentado e solução sugerida por todos os envolvidos.
Casos de Uso
Abaixo iremos ilustrar alguns cenários que se aplicariam alguns dos padrões mais utilizados em projetos corporativos.
Imagina que sua empresa foi contratada para desenvolver um software para gestão das correspondências de uma plataforma de ecommerce que inicialmente atende as demandas de compras de sua cidade realizando as entregas via motocicleta.
Factory Method
Factory Method é um padrão criacional que oferece uma estrutura para criar objetos em uma superclasse, permitindo que subclasses determinem o tipo específico de objeto a ser criado.
Após meses de operação e feedback dos clientes sobre a plataforma de e-commerce, a empresa optou por expandir seu catálogo de produtos. Para atender a essa demanda, surge a necessidade de incluir novas opções de transporte para entrega, como transporte terrestre por carro, e agora também transporte aéreo por avião, visando alcançar outros municípios e estados do país.
Adapter
O Adapter é um padrão de projeto estrutural que permite objetos com interfaces incompatíveis colaborarem entre si.
Considerando que agora o nosso serviço de logística da empresa de ecommerce possui uma integração com os serviços das empresas aéreas do país, será necessária uma implementação de recursos que possibilitem a adaptação e comunicação entre os dois sistemas.
Estas alternativas podem ser diversas, desde uma definição de estruturação de arquivos txt
sendo transmitidos via protocolo ftp até mesmo a implementação de protocolos http enviando e recebendo arquivos xml
e json
.
Strategy
O Strategy é um padrão de projeto comportamental que permite que você defina uma família de algoritmos, coloque-os em classes separadas, e faça os objetos deles intercambiáveis.
Imagina que a empresa de ecommerce agora irá oferecer um plano de serviço que foi carinhosamente chamado de compre e retire com alguns pontos de drive thru espalhados pela cidade para assim agilizar a entrega e ampliar a publicidade de sua marca.
Agora o sistema de gestão logística deverá ter duas estratégias para as entregas dos produtos comprados no site diante da opção de recebimento escolhida pelo cliente.
Antes de pensar em implementar os mais variados padrões de projetos recomendados pela comunidade, é extremamente relevante que você e seu time esteja plenamente engajado com os processos e operações com base no seguimento da empresa.
SOLID
SOLID é o acrônimo criado por Michael Feathers para representar os cinco princípios da programação orientada a objetos e design de códigos que tem por finalidade conduzir o programador para a criação de código com menos complexidade nas futuras manutenções.
Sigla | Significado | Tradução |
---|---|---|
S | Single Responsibility Principle | Princípio da Responsabilidade Única |
O | Open/closed Principle | Princípio do aberto/fechado |
L | Liskov Substitution Principle | Princípio da Substituição de Liskov |
I | Interface Segregation Principle | Princípio da Segregação de Interfaces |
D | Dependency Inversion Principle | Princípio da Inversão de Dependências |
Estes princípios separam responsabilidades diminuindo o
acoplamento
do código com intuito de facilitar na refatoração
, melhorias e evolução do seu projeto.
Por isso, antes de pensar em começar aplicando logo de cara todos estes princípios, repense no esforço que será necessário.Single Responsibility
Um dos princípios mais importante e acreditamos que o mais utilizados em todas as etapas de desenvolvimento do projeto, o Single Responsibility tem como finalidade definir e manter de forma bem distribuída e independente todo algoritmo e lógica de um software.
Em resumo, o Princípio da Responsabilidade Única sugere que uma classe deva ter somente um assunto, uma finalidade e ou objetivo, isso não quer dizer um único atributo ou método.
É muito comum em exemplos acadêmicos e até mesmo em projetos reais nos deparamos com abordagens semelhantes a esta abaixo:
Imagina que a empresa de ecommerce diante de seu sucesso de atuação no seguimento, resolveu diversificar suas atividades proporcionando para alguns de seus clientes a possibilidade dos mesmos criarem uma conta digital capaz de depositar, sacar e o principal, comprar online com muitos mais facilidade.
- Implementação convencional:
public class Conta {
double saldo;
void depositar(Double valorInserido){
saldo = saldo + valorInserido;
}
void sacar(Double valorSolicitado){
saldo = saldo - valorSolicitado;
}
double obterSaldo(){
return saldo;
}
String imprimirExtrato(){
//lógica que busca e apresenta todas as movimentações desta conta
return "";
}
}
Riscos
Precisamos começar a perceber os riscos de manutenibilidade e evolução do nosso software considerando alguns riscos conforme abaixo:
- A classe possui mais de uma responsabilidade (baixa coesão) ou faz praticamente tudo, sendo conhecida como a God Class ou Classe Deus;
- As responsabilidades estão misturadas e confusas dificultando uma interpretação diante da necessidade de uma manutenção;
- Aumento significativo do esforço necessário para implementar melhorias ou novas funcionalidades;
- Incertezas surgem quando confrontadas a lógica aplicada diante dos requisitos apresentados pelos usuários.
- Implementação com SRP:
Nesta abordagem grande parte do código que existe somente em uma classe, poderá ser distribuído em outras classes em seu projeto.
public class Conta {
private double saldo;
public void atualizarSaldo(Operacao operacao, Double valor){
if(Operacao.DEPOSITO == operacao)
saldo = saldo + valor;
else
saldo = saldo - valor;
}
public double obterSaldo(){
return saldo;
}
}
Diante de toda esta abordagem sobre SRP, agora somos capazes de compreender a importância de aplicar o princípio da responsabilidade única diante das classes de nosso projeto. Não esqueça: Uma classe deve ter um e somente um motivo para mudar.
Open-Closed
Na programação orientada a objeto, o princípio Open/Closed ou aberto/fechado estabelece que "entidades de software (classes, módulos, funções, etc.) devem ser abertas para extensão, mas fechadas para modificação"; isto é, a entidade pode permitir que o seu comportamento seja estendido sem modificar seu código-fonte.
Muitas das vezes este princípio exige a criação de interfaces
ou classes abstratas
para proporcionar o mecanismo de extensão comumente utilizada em linguagens orientadas a objetos. Vejamos o nosso contexto de contas ilustrando no princípio da Responsabilidade Única.
Vamos imaginar que a empresa de ecommerce quer agora permitir que seus clientes realize o saque de sua conta considerando os cenários abaixo:
- Quando o saque for realizado em um caixa eletrônico em seus pontos de apoio, o mesmo receberá o dinheiro em espécie;
- Novo recurso: Quando o saque for realizado pelo site, uma transação via pix deverá ser realizada de acordo com os dados cadastrais do
correntista
.
- Implementação convencional:
public class CaixaEletronico {
void sacar(Conta conta, String tipoSaque, Double valorSolicitado){
conta.atualizarSaldo(Operacao.SAQUE,valorSolicitado) ;
//novos comportamentos
if(tipoSaque == 'ESPECIE')
//o dinheiro será liberado pelo caixa eletrônico
else
//uma transação via pix será gerada creditando a conta pix do correntista.
}
}
- Implementação Open/Closed:
public interface Terminal {
void sacar(Conta conta, Double valorSolicitado);
//os métodos de uma interface ou de uma classe abstrata não possuem corpo.
}
Alterar classes já existentes para atender aos novos requisitos pode ampliar os riscos de introduzirmos bugs em nosso sistema. Então, lembre-se, aplicar a abstração via interfaces ou classes abstratas poderá deixar seu algoritmo mais flexível às nossas solicitações no projeto.
Liskov
Na programação orientada a objeto, o princípio da substituição de Liskov ou Liskov Substitution é uma definição particular para o conceito de subtipo, que diz que: Sua classe derivada
deve ser substituída por sua classe base
.
Considerando que nossa sistema precisará a partir de agora de exibir o nome de seus clientes considerando que estes clientes poderão ser pessoas físicas ou empresas em seu módulo de cadastros, logo, vejamos o contexto abaixo:
//o uso do conceito abstract é circustâncial
public abstract class Cliente {
String getNome();
}
Interface Segregation
No campo da engenharia de software, o princípio da segregação de Interface Interface Segregation afirma que nenhum cliente deve ser forçados a depender de métodos que não utiliza. ISP divide interfaces que são muito grandes em menores e mais específicas, para que os clientes só necessitem saber sobre os métodos que são de interesse para eles.
Ficou um pouco confuso? Calma, vamos já esclarecer, e para ajudar a entender, iremos utilizar o contexto que envolve o uso de interfaces utilizadas anteriormente.
A nossa empresa de ecommerce solicitou para o nosso time de desenvolvedores que agora será possível transferir o saldo em conta para outras contas cadastradas em sua plataforma, porém, somente via online ou caixa virtual.
public interface Terminal {
void sacar(Conta conta, Double valorSolicitado);
void transferir(Conta contaOrigem, Conta contaDestino, Double valorInformado); // ATENÇÃO
/**
* Se este recurso for adicionado nesta interface, todas as classes que implementam esta interface precisarão ser modificadas para atender a este novo requisito
*/
}
Dependency Inversion
No paradigma de orientação a objetos, o Princípio da inversão de dependência ou Dependency Inversion refere-se a uma forma específica de desacoplamento
de módulos de software que determina a inversão das relações de dependência.
O DIP reforça que é mais conveniente depender de abstrações e não de implementações com base em suas duas definições:
- Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações;
- As abstrações não devem depender de detalhes. Os detalhes devem depender das abstrações.
Antes de prosseguir, tenta em mente que,
inversão de dependências
não é o mesmo que injeção de dependências
, mesmo que ambos possuem a finalidade de desacoplar o código ou implementação real.Vamos imaginar que a empresa de ecommerce iniciou uma ação de marketing para divulgar um novo produto
e contratou três agências de marketing para cuidar da criação do material que será apresentado
em forma de três comerciais ao longo de uma semana.
public interface Agencia {
void divulgarProduto(Produto produto);
}