Executando verificação de segurança...
12

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.

Carregando publicação patrocinada...
2

Gostei bastantes dos exemplos dados, ajudaram muito no entendimento dos Patterns apresentados. Acho que deveria ser assim nas faculdades. Lembro que quando eu estava na faculdade cursando ADS, o professor da matéria que tratava de Design Patterns trazia eles com uns exemplos meio abstratos demais, nada haver com a realidade do dia-a-dia de uma programador. Isso acabava desmotivando a galera porque aprender isso no início da jornada na programação já exige bastante do aluno e com exemplos que não ajudam, a coisa só tende a piorar.

Parabéns pelo texto e pelas escolhas dos exemplos!

-1