design patterns

Design Patterns: seu código com mais qualidade e elegância

Se você trabalha com desenvolvimento de software e, assim como eu, REALMENTE ama o que faz, você deve conhecer ou pelo menos ter ouvido falar sobre Design Patterns.

O assunto inclusive faz parte do programa do curso de programação da Digital House, que no caso, eu, Franklin Barreto, ministro aulas.

Talvez você já tenha até implementado algum deles sem saber e, se for esse o caso, provavelmente irá se sentir um “super dev” assim como já aconteceu comigo.

Imagine o sentimento de descobrir que aquela solução que você chegou havia sido catalogada há muito tempo com um padrão de design por experts no desenvolvimento de software orientado a objetos.

Descubra os paradigmas de programação mais importantes

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.

Clean code: o que é e boas práticas de desenvolvimento

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.

É isso aí pessoal, bons estudos e até mais!

Compartilhar
You May Also Like