Introdução aos Padrões de Fábrica
Em linguagens orientadas a objetos como o Java, a instanciação direta de classes utilizando o operador new gera um alto grau de acoplamento. Se a implementação de uma classe precisar ser alterada, todos os pontos do código onde ela foi instanciada terão que ser refatorados, violando o Princípio Aberto/Fechado (Open/Closed Principle). Ao delegar a criação de objetos a uma "fábrica", o código cliente interage apenas com a interface da fábrica, promovendo o desacoplamento e facilitando a manutenção e expansão do sistema.
Para ilustrar esses conceitos, utilizaremos o domínio de uma cafeteria que oferece diferentes tipos de bebidas e acompanhamentos.
Padrão Simple Factory (Fábrica Simples)
Embora não faça parte dos 23 padrões de projeto clássicos do GoF, a Fábrica Simples é uma convenção de programação amplamente adotada para centralizar a lógica de criação de objetos.
Estrutura e Implementação
A estrutura básica envolve um produto abstrato, produtos concretos e uma classe fábrica que decide qual produto instanciar com base em um parâmetro.
package cafeteria.bebidas;
public abstract class Bebida {
public abstract String getNome();
public void adicionarAcucar() {
System.out.println("Adicionando açúcar à bebida.");
}
}
package cafeteria.bebidas;
public class Espresso extends Bebida {
@Override
public String getNome() {
return "Café Espresso";
}
}
package cafeteria.bebidas;
public class Mocha extends Bebida {
@Override
public String getNome() {
return "Café Mocha";
}
}
package cafeteria.fabricas;
import cafeteria.bebidas.Bebida;
import cafeteria.bebidas.Espresso;
import cafeteria.bebidas.Mocha;
public class FabricaDeBebidas {
public Bebida preparar(String tipo) {
if (tipo == null) {
throw new IllegalArgumentException("O tipo de bebida não pode ser nulo.");
}
switch (tipo.toLowerCase()) {
case "espresso":
return new Espresso();
case "mocha":
return new Mocha();
default:
throw new UnsupportedOperationException("Bebida não disponível no cardápio.");
}
}
}
Análise e Limitações
A Fábrica Simples encapsula a lógica de criação, isolando o cliente (ex: a classe da loja) das implementações concretas. No entanto, a adição de novos produtos exige a modificação da classe FabricaDeBebidas, o que fere o Princípio Aberto/Fechado. Uma variação comum é o uso de métodos estáticos na fábrica (Static Factory Method), eliminando a necessidade de instanciar a própria fábrica, mas mantendo as mesmas limitações de extensibilidade.
Padrão Factory Method (Método Fábrica)
O padrão Factory Method resolve as limitações da Fábrica Simples ao definir uma interface para criar um objeto, mas deixa as subclasses decidirem qual classe instanciar. Isso adia a instanciação para as subclasses.
Implementação com Factory Method
Neste modelo, criamos uma hierarquia de fábricas que espelha a hierarquia de produtos.
package cafeteria.fabricas;
import cafeteria.bebidas.Bebida;
public interface CriadorDeBebida {
Bebida fabricar();
}
package cafeteria.fabricas;
import cafeteria.bebidas.Bebida;
import cafeteria.bebidas.Espresso;
public class CriadorDeEspresso implements CriadorDeBebida {
@Override
public Bebida fabricar() {
return new Espresso();
}
}
package cafeteria.fabricas;
import cafeteria.bebidas.Bebida;
import cafeteria.bebidas.Mocha;
public class CriadorDeMocha implements CriadorDeBebida {
@Override
public Bebida fabricar() {
return new Mocha();
}
}
package cafeteria.loja;
import cafeteria.bebidas.Bebida;
import cafeteria.fabricas.CriadorDeBebida;
public class Cafeteria {
private CriadorDeBebida criador;
public void definirCriador(CriadorDeBebida criador) {
this.criador = criador;
}
public Bebida servirBebida() {
Bebida bebida = criador.fabricar();
bebida.adicionarAcucar();
return bebida;
}
}
Vantagens e Desvantagens
A principal vantagem é a aderência ao Princípio Aberto/Fechado: para adicionar um novo produto, basta criar uma nova subclasse de produto e sua respectiva fábrica, sem alterar o código existente. A desvantagem é a proliferação de classes, o que pode aumentar a complexidade estrutural do sistema.
Padrão Abstract Factory (Fábrica Abstrata)
Enquanto o Factory Method lida com a criação de um único tipo de produto, o Abstract Factory fornece uma interface para criar famílias de objetos relacionados ou dependentes sem especificar suas classes concrteas. Isso é útil quando o sistema precisa lidar com "famílias de produtos" (ex: combinações de bebidas e sobremesas de uma mesma temática).
Estrutura e Cenário de Aplicação
Imagine que a cafeteria agora oferece "Combos" que incluem uma bebida e uma sobremesa. Um combo italiano inclui Espresso e Tiramisu, enquanto um combo americano inclui Mocha e Brownie.
package cafeteria.sobremesas;
public abstract class Sobremesa {
public abstract String getNome();
}
public class Tiramisu extends Sobremesa {
@Override
public String getNome() { return "Tiramisu"; }
}
public class Brownie extends Sobremesa {
@Override
public String getNome() { return "Brownie"; }
}
package cafeteria.fabricas;
import cafeteria.bebidas.Bebida;
import cafeteria.sobremesas.Sobremesa;
public interface FabricaDeCombo {
Bebida criarBebida();
Sobremesa criarSobremesa();
}
package cafeteria.fabricas;
import cafeteria.bebidas.Bebida;
import cafeteria.bebidas.Espresso;
import cafeteria.sobremesas.Sobremesa;
import cafeteria.sobremesas.Tiramisu;
public class FabricaComboItaliano implements FabricaDeCombo {
@Override
public Bebida criarBebida() {
return new Espresso();
}
@Override
public Sobremesa criarSobremesa() {
return new Tiramisu();
}
}
package cafeteria.fabricas;
import cafeteria.bebidas.Bebida;
import cafeteria.bebidas.Mocha;
import cafeteria.sobremesas.Sobremesa;
import cafeteria.sobremesas.Brownie;
public class FabricaComboAmericano implements FabricaDeCombo {
@Override
public Bebida criarBebida() {
return new Mocha();
}
@Override
public Sobremesa criarSobremesa() {
return new Brownie();
}
}
Análise do Abstract Factory
Este padrão garante que os produtos de uma mesma família sejam sempre utiliazdos em conjunto. A grande desvantagem surge ao adicionar um novo tipo de produto à família (ex: adicionar um "Salgado" ao combo), pois todas as interfaces de fábricas e suas implementações concretas precisarão ser modificadas.
Desacoplamento com Configuração Externa e Reflexão
Para mitigar o acoplamento entre a fábrica e as classes concretas, podemos combinar o padrão Simple Factory com leitura de arquivos de configuração e a API de Reflection do Java. Isso permite adicionar novos produtos apenas alterando o arquivo de configuração, sem recompilar a classe fábrica.
Arquivo de Configuração
# cardapio.properties
espresso=cafeteria.bebidas.Espresso
mocha=cafeteria.bebidas.Mocha
Fábrica Dinâmica
package cafeteria.fabricas;
import cafeteria.bebidas.Bebida;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class FabricaDinamica {
private static final Map<String, Bebida> cache = new HashMap<>();
static {
Properties propriedades = new Properties();
try (InputStream input = FabricaDinamica.class.getClassLoader()
.getResourceAsStream("cardapio.properties")) {
propriedades.load(input);
for (String chave : propriedades.stringPropertyNames()) {
String nomeClasse = propriedades.getProperty(chave);
Class<?> clazz = Class.forName(nomeClasse);
Bebida instancia = (Bebida) clazz.getDeclaredConstructor().newInstance();
cache.put(chave, instancia);
}
} catch (Exception e) {
throw new RuntimeException("Falha ao inicializar a fábrica dinâmica", e);
}
}
public static Bebida obterBebida(String tipo) {
Bebida bebida = cache.get(tipo);
if (bebida == null) {
throw new IllegalArgumentException("Bebida não encontrada: " + tipo);
}
return bebida;
}
}
Ao carregar as classes via Class.forName e armazená-las em um cache durante a inicialização estática, o sistema torna-se altamente flexível. A adição de novas bebidas exige apenas a inclusão de novas entradas no arquivo .properties.
Aplicações no JDK do Java
Os padrões de fábrica são ubíquos na biblioteca padrão do Java. Um exemplo clássico é o método iterator() da interface Collection. A interface Collection atua como o criador abstrato, enquanto classes concretas como ArrayList implementam o método para retornar uma instância de seu iterador interno específico.
Outros exemplos notáveis incluem:
java.util.Calendar.getInstance(): Utiliza uma fábrica estática para retornar a implementação de calendário apropriada com base na localidade e fuso horário.java.text.NumberFormat.getInstance(): Retorna formatadores de números específicos para diferentes regiões, ocultando a complexidade das classes concretas de formatação.