Construindo Flexibilidade com o Padrão Factory: Uma Perspectiva Prática

O design pattern Factory, ou conhecido também como Factory Pattern é um padrão de projeto criacional, sua principal utilização é disponibilizar uma interface para criação de objetos, mas que também permite que subclasses alterem o tipo de objetos que podem ser criados.

Devemos utilizar esse padrão quando você necessita de uma lógica complexa na criação de um objeto ou quando você precisa de definir o tipo de objeto com base em alguma informação como um parâmetro, por exemplo.

Usando Java é possível implementar esse padrão de diversas formas, mais a seguir vamos abordar uma das formas mais comuns, mas lembrando que esse é um campo aberto e você pode usar as diretrizes do padrão para personalizar e fazer com que ele atenda a sua regra de negócio.

Exemplo 1: Abordagem didática do Factory

Para exemplificar sua implementação vamos fazer um cenário bem simples, e com um exemplo bem comum, bom pelo menos pra mim quando aprendi orientação a objeto, sempre usavam o exemplos como animais para me explicar sobre herança.

Um ponto bem relevante é que para adotar esse tipo de padrão de projeto é necessário ter um certo domínio sobre a regra de negócio, para que você implemente ele da melhor forma.

Veja o exemplo a seguir, temos duas classes basicamente Cachorro e Gato e elas por sua vez são implementações de uma interface chamada Animal, bom até aí você já deve ter se lembrado dos exemplos.

public interface Animal {
  void fazerSom();
}
Animal.java
public class Cachorro implements Animal {

	@Override
	public void fazerSom() {
		System.out.println("Latindo...");
	}

}
Cachorro.java
public class Gato implements Animal {

	@Override
	public void fazerSom() {
		System.out.println("Miando...");
	}

}
Gato.java

O próximo passo é criar uma fábrica que consiga criar cada um desses animais, bom geralmente usamos essa abordagem com criações de objeto com lógicas mais complexas, mas para o exemplo vamos seguir na simplicidade.

Primeiro vamos definir uma interface para que nossas fábricas sigam o padrão:

public interface AnimalFactory {

	Animal criarAnimal();
}
AnimalFactory.java

Note que nossa fábrica possui apenas um método que cria um animal, e sempre trabalhamos com as interfaces para garantir um desacoplamento, agora vamos criar implementações dessa fábrica com sua regra específica.

Fábrica de cachorro:

public class CachorroFactory implements AnimalFactory {

	@Override
	public Animal criarAnimal() {
		return new Cachorro();
	}

}
CachorroFactory.java

Fábrica de gato:

public class GatoFactory implements AnimalFactory {

	@Override
	public Animal criarAnimal() {
		return new Gato();
	}

}
GatoFactory.java

Note que nas implementações específicas precisamos apenas instanciar as classes das quais elas são responsáveis.

Agora vamos demonstrar como utilizar essas fábricas para a nossa implementação, primeiro exemplo vamos criar um cachorro.

public class Exemplo {

	public static void main(String[] args) {
		
		AnimalFactory factory = new CachorroFactory();
		Animal cachorro = factory.criarAnimal();
		cachorro.fazerSom();
		
	}
}
Exemplo.java

Viu como é simples a lógica de criação do animal acaba ficando encapsulada dentro da fábrica, agora vejamos como criar um gato.

public class Exemplo {

	public static void main(String[] args) {
		
		AnimalFactory factory = new GatoFactory();
		Animal gato = factory.criarAnimal();
		gato.fazerSom();
		
	}
}
Exemplo.java

Podemos testar esse exemplo de uma forma melhor, recebendo um parâmetro que vai definir a criação de cada animal.

public class Exemplo {

	public static void main(String[] args) {
		
		Scanner scanner = new Scanner(System.in);
		System.out.println("Digite o tipo de animal: ");
		String tipo = scanner.nextLine().toUpperCase().trim();
		
		AnimalFactory animalFactory = null;
		
		switch (tipo) {
  		case "CACHORRO":
  			animalFactory = new CachorroFactory();
  			break;
  		case "GATO":
  			animalFactory = new GatoFactory();
  			break;
		}
		
		Animal animal = animalFactory.criarAnimal();
		animal.fazerSom();
		
		scanner.close();
	}
}
Exemplo.java

Note que em momento algum referenciamos alguma classe concreta, estamos trabalhando sempre com as abstrações, isso torna o nosso código muito poderoso, porque podemos acrescentar implementações sem ferir a lógica já criada e não corremos risco de desconstruir de alguma forma as implementações que já foram feitas.

Exemplo 2: abordagem mais realística

Bom falando assim é bem simples pensar em uma maneira de aplicar, mas acho bem difícil cair para você um problema para criar classes baseadas em animais, então vamos tentar usar um problema um pouco mais plausível.

Digamos que você está trabalhando em um sistema que vai exigir pagamentos, e na atual situação, você está recebendo dois tipos de pagamento, cartão de crédito ou cartão de débito, vamos tentar utilizar esse padrão de projeto em um problema desse.

Como nosso próximo exemplo vamos imaginar que você está trabalhando em um sistema de pagamentos, que é capaz de receber pagamentos de diversos tipos diferentes, na situação atual precisamos implementar dois métodos, o de pagamentos via cartão de crédito e o de pagamento utilizando o PayPal, vamos tentar aplicar o mesmo padrão nessa solução.

Vamos criar uma interface comum que representará o nosso método de pagamento:

public interface MetodoPagamento {

	void processarPagamento(BigDecimal valor);
}
MetodoPagamento.java

Cada método de pagamento do nosso sistema deve implementar essa interface, veja como construímos:

Método de pagamento Cartão de crédito

public class CartaoCredito implements MetodoPagamento {

	private String nomeTitular;
	private String numero;
	private String cvv;
	
	public CartaoCredito(String nomeTitular, String numero, String cvv) {
		super();
		this.nomeTitular = nomeTitular;
		this.numero = numero;
		this.cvv = cvv;
	}

	@Override
	public void processarPagamento(BigDecimal valor) {
		System.out.println(String.format("Processando pagamento de %s com valor de %f",     this.nomeTitular, valor));
	}

}
CartaoCredito.java

Método de pagamento PayPal

public class PayPal implements MetodoPagamento {

	private String usuario;
	
	public PayPal(String usuario) {
		super();
		this.usuario = usuario;
	}



	@Override
	public void processarPagamento(BigDecimal valor) {
		System.out.println(String.format("Processando pagamento com PayPal para %s com valor %f", this.usuario, valor));
	}

}
PayPal.java

Agora vamos implementar a abstração da nossa fábrica, todas as fábricas terão que seguir esse padrão:

public abstract class ProcessadorPagamento {

	abstract MetodoPagamento criarMetodoPagamento(Map<String, Object> informacoes);
	
	void executarPagamento(Map<String, Object> informacoes, BigDecimal valor) {
		MetodoPagamento metodoPagamento = criarMetodoPagamento(informacoes);
		metodoPagamento.processarPagamento(valor);
	}
}
ProcessadorPagamento.java

Note que ao invés de usar uma interface como no exemplo anterior, utilizamos uma classe abstrata, isso porque de fato ela já executa um pouco de lógica no método de “executarPagamento”.

Agora precisamos implementar cada fábrica especializada em criar o seu método de pagamento:

Cartão de crédito

public class CartaoCreditoProcessadorPagamento extends ProcessadorPagamento {

	private static final String CHAVE_NOME_TITULAR = "nomeTitular";
	private static final String CHAVE_NUMERO = "numero";
	private static final String CHAVE_CVV = "cvv";
	
	@Override
	MetodoPagamento criarMetodoPagamento(Map<String, Object> informacoes) {
		String nomeTitular = (String) informacoes.get(CHAVE_NOME_TITULAR);
		String numeroCartao = (String) informacoes.get(CHAVE_NUMERO);
		String cvv = (String) informacoes.get(CHAVE_CVV);
		return new CartaoCredito(nomeTitular, numeroCartao, cvv);
	}

}
CartaoCreditoProcessadorPagamento.java

PayPal

public class PayPalProcessadorPagamento extends ProcessadorPagamento {

	private static final String CHAVE_USUARIO = "usuario";
	
	@Override
	MetodoPagamento criarMetodoPagamento(Map<String, Object> informacoes) {
		String usuario = (String) informacoes.get(CHAVE_USUARIO);
		return new PayPal(usuario);
	}

}
PayPalProcessadorPagamento.java

E aqui temos um ponto bem importante de observar, perceba que cada método de pagamento utiliza-se de informações diferentes, cada um tem o seu jeito próprio de ser construído, para abstrair isso utilizamos uma classe genérica do Java para transitar informações, pois cada fábrica saberá quais informações pegar.

Importante lembrar que essas informações poderiam trafegar dentro de uma classe criada também.

Para deixar nossa lógica mais organizada, também criamos um Enum classificando os tipos de métodos de pagamento.

public enum TipoPagamento {

	CARTAO_CREDITO,
	PAYPAL;
}
TipoPagamento.java

E por fim vamos implementar a classe que utilizará as fábricas e executará os métodos.

public class ExemploPagamento {

	public static void main(String[] args) {
		Map<String, Object> informacoesPagamento = new HashMap<String, Object>();
		ProcessadorPagamento processadorPagamento = selecionarTipo(TipoPagamento.CARTAO_CREDITO);
		BigDecimal valor = BigDecimal.valueOf(0.39);
		MetodoPagamento metodoPagamento = processadorPagamento.criarMetodoPagamento(informacoesPagamento);
		metodoPagamento.processarPagamento(valor);
	}

	private static ProcessadorPagamento selecionarTipo(TipoPagamento tipo) {
		switch (tipo) {
		case CARTAO_CREDITO:
			return new CartaoCreditoProcessadorPagamento();
		case PAYPAL:
			return new PayPalProcessadorPagamento();
		default:
			throw new IllegalArgumentException("Método ainda não implementado");
		}
	}
}
ExemploPagamento.java

No exemplo acima utilizamos o cartão de crédito e estamos passando as informações em uma variável comum, mas essas informações podem vir via parâmetros de uma chamada REST por exemplo.

Conclusão

Se observar bem, essa solução é muito escalável, para incrementar essa solução adicionando mais métodos é simples, e não impacta os que já foram implementados, as responsabilidades ficam completamente segregadas, e na hora de dar manutenção nesse tipo de implementação é bem simples e seguro já que cada implementação está de uma certa forma isolada uma da outra.

Links uteis

Mauricio Lima
Mauricio Lima

Bacharel em Ciência da Computação, profissional dedicado ao desenvolvimento de software e entusiasta da tecnologia.

Artigos: 65