O que são Design Patterns?
Design patterns ou padrões de design são soluções já testadas para problemas recorrentes no desenvolvimento de software, que deixam seu código mais manutenível e elegante, pois essas soluções se baseiam em um baixo acoplamento.
Acredito que o livro mais famoso referente a esse assunto seja o “Design Patterns: Elements of Reusable Object-Oriented Software” nomeado no Brasil “Padrões de Projeto — Soluções Reutilizáveis de Software Orientado a Objetos”.
Nesse livro, os autores Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides catalogaram 23 design patterns divididos em três categorias: criacionais, estruturais e comportamentais.
Para que serve Design Patterns?
Como dito anteriormente, o uso de Design Patterns deixará seu código mais fácil de ser mantido e testado, além da segurança de serem soluções já testadas repetidamente e que foram adotadas por terem dado certo.E mais, as discussões técnicas serão mais fáceis tendo em vista que todos estarão falando uma mesma língua, sem esquecer, é claro, de um código mais elegante.
Quando usar Design Patterns?
design patterns o que são Essa é a pergunta de um milhão de dólares para quem está aprendendo.Quando a gente vê tudo o que pode ser alcançado com a utilização dessas técnicas, sentimos o desejo de sair usando a torto e a direito, mas podemos acabar deixando complexo algo que seria simples.
A partir do momento em que realmente entendemos como eles funcionam, temos um melhor discernimento para saber a hora de aplicá-los.
Design Patterns de criação
Como o próprio nome sugere, esses padrões nos ajudam na criação de objetos, eles abstraem a criação de um objeto ajudando a tornar o sistema independente de como os objetos são criados:- Factory Method
- Abstract Factory
- Builder
- Prototype
- Singleton
Vamos ver um exemplo criando uma Factory Method de conexões com o banco de dados.
Primeiro criamos uma interface Conexao com um método que nos retorna uma connection:
public interface Conexao {
Connection getConnection() throws SQLException;
}
Agora criamos uma implementação concreta que se conecta com um banco Mysql e implementa nossa interface Conexao:
public class MysqlConexao implements Conexao {
@Override
public Connection getConnection() throws SQLException {
MysqlDataSource source = new MysqlDataSource();
source.setUrl("jdbc:mysql://localhost/loja");
source.setUser("root");
source.setPassword("");
return source.getConnection();
}
}
Vamos criar agora a classe OracleConexao:
public class OracleConexao implements Conexao {
@Override
public Connection getConnection() throws SQLException {
OracleDataSource source = new OracleDataSource();
source.setDatabaseName("Banco");
source.setURL("jdbc:oracle:thin:@localhost:1521");
source.setUser("root");
source.setPassword("1234");
return source.getConnection();
}
}
E por fim criamos nossa fábrica de conexões:
public abstract class ConexaoFactory {
private ConexaoFactory() {}
public static Conexao getConexao(String tipo) {
switch (tipo) {
case "MYSQL":
return new MysqlConexao();
case "ORACLE":
return new OracleConexao();
default:
throw new RuntimeException("Banco não existe");
}
}
}
E aqui um exemplo de uso:
public class TestaConexao {
public static void main(String[] args) throws SQLException {
Conexao conexao = ConexaoFactory.getConexao("MYSQL");
conexao.getConnection().prepareStatement("Select tabela que você quer buscar");
}
}
As implementações ficaram encapsuladas em suas respectivas classes concretas e o trabalho de criação ficou na factory, assim, se houver a necessidade de conexão com um outro banco, como postgres, por exemplo, é só criar a implementação e adicionar a chamada em nossa ConexaoFactory.
Design Patterns de estrutura
O trabalho deles é lidar com a composição de classes ou objetos, facilitando o design do sistema, identificando maneiras de realizar o relacionamento entre as entidades.- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
Imagine que você aprendeu com sua avó uma receita especial de vitamina de abacate e resolveu começar a vender.
Como era um sistema simples só com um produto e você tinha um pouco de conhecimento em programação, você decidiu fazer por conta própria.
As classes foram modeladas e, como único produto, você criou a classe abacate. Apesar dos negócios estarem indo bem, os clientes começaram a pedir outros sabores como mamão e banana por exemplo.
Você criou as outras classes sem nenhuma complicação, mas depois começaram a pedir por mix de sabores.
Criar AbacateComMamao ou BananaComMamao iria ser um problema, pois poderiam surgir várias outras combinações e ficaria algo gigante, feio e complexo.
Aí que o decorator entra para nos ajudar e veremos como com o código a seguir.
Primeiro criamos uma classe abstrata chamada Vitamina:
public abstract class Vitamina {
private Vitamina vitamina;
public Vitamina(Vitamina vitamina) {
this.vitamina = vitamina;
}
public Vitamina() {}
protected abstract String getSaborEspecifico();
public String getSabor() {
return vitamina != null ? vitamina.getSabor().concat(" " + getSaborEspecifico()) : getSaborEspecifico();
}
}
Criamos um construtor que recebe como argumento a própria classe para o caso de ser um mix de sabores e um construtor sem argumento para sabor único ou não ficarmos sem um fim.
No método getSabor verificamos se vitamina é nula, e caso seja verdadeira, pegamos o sabor específico. Caso contrário, concatenamos, tendo assim os outros sabores.
Agora, criamos as classes específicas que estendem de Vitamina e implementam o sabor específico que foi definido como abstrato:
public class Abacate extends Vitamina {
public Abacate(Vitamina vitaminaInterface) {
super(vitaminaInterface);
}
public Abacate() {
}
@Override
public String getSaborEspecifico() {
return "abacate";
}
}
public class Mamao extends Vitamina {
public Mamao(Vitamina vitaminaInterface) {
super(vitaminaInterface);
}
public Mamao() {
}
@Override
public String getSaborEspecifico() {
return "mamao";
}
}
Aqui nossa classe de teste.
public class TestaVitamina {
public static void main(String[] args) {
Vitamina vitamina = new Abacate();
System.out.println(vitamina.getSabor());
vitamina = new Mamao(vitamina);
System.out.println(vitamina.getSabor());
vitamina = new Banana(vitamina);
System.out.println(vitamina.getSabor());
}
}
Ao rodar o código teremos:
- Abacate
- Abacate Mamão
- Abacate Mamão Banana
Design Patterns de comportamento
Os padrões de comportamento definem a maneira como classes ou objetos interagem e distribuem responsabilidades:
- Interpreter
- Template Method
- Chain of Responsibility
- Command
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Visitor
Trabalhei em uma empresa que lidava com banco e nós precisávamos gerar comprovantes de diversos tipos como folha, tributos e transferências. Quando coloquei a mão na massa, me deparei com um código parecido com isso:
package strategy;
public class GeradorComprovante {
public void geraComprovante(String tipo) {
if (tipo == "folha") {
System.out.println("Comprovante de pagamento de folha");
} else if (tipo == "tributos") {
System.out.println("Comprovamente de pagamento de tributos");
} else if (tipo == "transferencia") {
System.out.println("Comprovante de transferência");
}
}
}
Esse é um código que tem uma chance enorme de não parar de crescer e a lógica dentro dele também pode ser bem complexa como era o caso. Como eu resolvi esse problema? Strategy.
Foi criada a interface ComprovanteInterface.
package strategy;
public interface ComprovanteInterface {
public String getComprovante();
}
E em seguida as respectivas classes Transferencia e FolhaPagamento que implementavam a ComprovanteInterface.
package strategy;
public class Transferencia implements ComprovanteInterface {
@Override
public String getComprovante() {
return "Comprovante de transferência";
}
}
package strategy;
public class FolhaPagamento implements ComprovanteInterface {
@Override
public String getComprovante() {
return "Comprovante de pagamento de folha";
}
}
E agora nem precisamos mais da classe GeradorComprovante, visto que cada classe sabe como gerar seu próprio comprovante.
package strategy;
public class TesteComprovante {
public static void main(String[] args) {
ComprovanteInterface transferencia = new Transferencia();
ComprovanteInterface folha = new FolhaPagamento();
System.out.println(folha.getComprovante());
System.out.println(transferencia.getComprovante());
}
}
Acho que isso foi o suficiente para dar uma ideia de que se quer ser um desenvolvedor de alto nível, ter em sua “caixa de ferramentas” o conhecimento sobre Design Patterns é fundamental.
Isso aumentará a qualidade do seu código e também te ajudará a pensar em novas formas de resolver problemas antigo melhorando suas habilidades como desenvolvedor.