Desacoplamento de Código com Padrões de Projeto de Fábrica em Java

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.

Tags: java design-patterns factory-pattern abstract-factory factory-method

Publicado em 7-25 08:51