Padrão Factory em Java OOP: criação de objetos e centralização de variações

Capítulo 14

Tempo estimado de leitura: 8 minutos

+ Exercício

O que é o padrão Factory (e por que ele existe)

Factory é uma forma de centralizar a criação de objetos quando instanciar diretamente com new começa a gerar problemas: construção complexa, escolha entre implementações, repetição de regras e acoplamento. Em vez de espalhar new ImplementacaoX(...) pelo código, você delega a criação para um ponto único e bem nomeado.

Quando usar (sinais práticos)

  • Construção complexa: muitos parâmetros, validações, dependências, configuração condicional, defaults.
  • Escolha entre implementações: dependendo de um tipo, canal, ambiente, feature flag, região, etc.
  • Redução de acoplamento: o código cliente deveria depender de uma abstração (interface/classe base), não de classes concretas.
  • Regras de criação repetidas: o mesmo if/else e os mesmos parâmetros aparecem em vários lugares.
  • Testabilidade: você quer trocar facilmente uma implementação real por uma fake/stub em testes.

Simple Factory vs Factory Method

Simple Factory (fábrica simples)

É uma classe (ou método) que recebe algum critério (por exemplo, um enum) e devolve a implementação correta. Não é um “padrão GoF” formal, mas é extremamente útil e comum.

Factory Method

É um padrão formal: uma classe base define um método de criação (factory method) e subclasses decidem qual produto concreto instanciar. Ele é útil quando a escolha do produto varia por “família” de criadores (por exemplo, por ambiente, por tenant, por integração).

Exemplo prático 1: Notificadores com Simple Factory

Objetivo: criar notificadores (Email, SMS, Push) sem espalhar new e sem o cliente conhecer classes concretas.

Passo 1: definir o contrato

public interface Notificador {    void enviar(String destino, String mensagem);}

Passo 2: implementar variações

public final class NotificadorEmail implements Notificador {    private final ServicoEmail servico;    public NotificadorEmail(ServicoEmail servico) {        this.servico = servico;    }    @Override    public void enviar(String destino, String mensagem) {        servico.enviarEmail(destino, mensagem);    }}public final class NotificadorSms implements Notificador {    private final GatewaySms gateway;    public NotificadorSms(GatewaySms gateway) {        this.gateway = gateway;    }    @Override    public void enviar(String destino, String mensagem) {        gateway.enviarSms(destino, mensagem);    }}public final class NotificadorPush implements Notificador {    private final PushClient client;    public NotificadorPush(PushClient client) {        this.client = client;    }    @Override    public void enviar(String destino, String mensagem) {        client.enviarPush(destino, mensagem);    }}

Passo 3: criar um tipo para a escolha

public enum CanalNotificacao { EMAIL, SMS, PUSH }

Passo 4: implementar a Factory (centralizando variações)

public final class NotificadorFactory {    private final ServicoEmail servicoEmail;    private final GatewaySms gatewaySms;    private final PushClient pushClient;    public NotificadorFactory(ServicoEmail servicoEmail,                             GatewaySms gatewaySms,                             PushClient pushClient) {        this.servicoEmail = servicoEmail;        this.gatewaySms = gatewaySms;        this.pushClient = pushClient;    }    public Notificador criar(CanalNotificacao canal) {        return switch (canal) {            case EMAIL -> new NotificadorEmail(servicoEmail);            case SMS -> new NotificadorSms(gatewaySms);            case PUSH -> new NotificadorPush(pushClient);        };    }}

Passo 5: usar no cliente (sem conhecer concretos)

public final class CentralDeMensagens {    private final NotificadorFactory factory;    public CentralDeMensagens(NotificadorFactory factory) {        this.factory = factory;    }    public void notificar(CanalNotificacao canal, String destino, String mensagem) {        Notificador notificador = factory.criar(canal);        notificador.enviar(destino, mensagem);    }}

O que melhorou

  • O cliente depende de Notificador e da factory, não de NotificadorEmail/NotificadorSms/NotificadorPush.
  • Se amanhã surgir WHATSAPP, a mudança fica concentrada na factory (e na nova implementação).
  • Regras de criação (dependências, defaults) ficam em um lugar só.

Critérios de design: o que fica na Factory e o que fica no construtor

DecisãoColocar no construtorColocar na factory
Invariantes do objetoSim. Validações essenciais para o objeto existir corretamente.Não como substituto. A factory pode pré-validar, mas o construtor deve garantir.
Escolha entre implementaçõesNão. Construtor não deveria decidir “qual classe eu sou”.Sim. A factory decide qual implementação criar.
Defaults e configuraçãoSomente se forem intrínsecos ao tipo.Sim, quando dependem de contexto (ambiente, canal, feature flag).
Montagem de dependênciasEvite. Construtor deve receber dependências prontas.Sim. A factory pode compor dependências e passá-las ao construtor.
Cache/reuso de instânciasNão é papel do construtor.Pode ser. A factory pode retornar singleton, pool, ou instância compartilhada quando fizer sentido.

Regra prática: construtor garante consistência do objeto; factory orquestra variações e contexto de criação.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Testabilidade: como testar código com Factory

Teste do cliente (sem depender de implementações reais)

Uma vantagem de centralizar a criação é poder substituir a factory por uma versão de teste que devolve doubles previsíveis.

public final class NotificadorFactoryFake extends NotificadorFactory {    private final Notificador fixo;    public NotificadorFactoryFake(Notificador fixo) {        super(null, null, null);        this.fixo = fixo;    }    @Override    public Notificador criar(CanalNotificacao canal) {        return fixo;    }}

Alternativa mais simples: definir uma interface para a factory e injetar uma implementação fake.

public interface CriadorDeNotificador {    Notificador criar(CanalNotificacao canal);}public final class NotificadorFactory implements CriadorDeNotificador {    // mesma implementação de antes}

Teste da factory (garantindo o mapeamento)

Teste a factory como uma unidade: dado um canal, retorna o tipo esperado, e injeta as dependências corretas.

// Exemplo conceitual (sem framework):NotificadorFactory factory = new NotificadorFactory(servicoEmail, gatewaySms, pushClient);Notificador n = factory.criar(CanalNotificacao.SMS);assert n instanceof NotificadorSms;

Exemplo prático 2: Factory Method para escolher família de criação

Agora o critério não é apenas “qual canal”, mas “qual ambiente/integração” define uma família de objetos. Exemplo: em desenvolvimento você quer um notificador que apenas registra em log; em produção, notificador real.

Passo 1: Creator com factory method

public abstract class NotificadorCreator {    public final Notificador obterNotificador(CanalNotificacao canal) {        // template simples: validações comuns podem ficar aqui        return criarNotificador(canal);    }    protected abstract Notificador criarNotificador(CanalNotificacao canal);}

Passo 2: Criadores concretos

public final class NotificadorCreatorDev extends NotificadorCreator {    @Override    protected Notificador criarNotificador(CanalNotificacao canal) {        return (destino, mensagem) -> System.out.println("[DEV] " + canal + ": " + destino + " - " + mensagem);    }}public final class NotificadorCreatorProd extends NotificadorCreator {    private final NotificadorFactory factory;    public NotificadorCreatorProd(NotificadorFactory factory) {        this.factory = factory;    }    @Override    protected Notificador criarNotificador(CanalNotificacao canal) {        return factory.criar(canal);    }}

Uso

NotificadorCreator creator = ambiente.equals("prod")    ? new NotificadorCreatorProd(factoryReal)    : new NotificadorCreatorDev();Notificador n = creator.obterNotificador(CanalNotificacao.EMAIL);n.enviar("user@dominio.com", "Olá!");

Esse formato é útil quando você quer variar a estratégia de criação por contexto (ambiente, cliente, tenant), mantendo um ponto de extensão claro.

Armadilhas comuns (e como evitar)

  • Factory “Deus”: uma factory gigante criando tudo do sistema. Prefira factories por domínio (ex.: NotificadorFactory, ParserFactory), com responsabilidade coesa.
  • Factory escondendo regras de negócio: a factory deve decidir criação, não regras do tipo “quem pode receber notificação”.
  • Excesso de parâmetros: se a factory recebe muitos parâmetros por chamada, talvez ela devesse ser um objeto com dependências no construtor e um método criar(...) mais simples (como no exemplo).
  • Switch espalhado: se o switch aparece em vários lugares, ele pertence a uma factory (ou a um registro/mapa de criadores).

Exercício: substituir new espalhado por uma factory bem nomeada

Cenário: você tem um serviço que calcula frete e escolhe a estratégia com new em vários pontos.

Código inicial (para refatorar)

public enum TipoFrete { ECONOMICO, EXPRESSO }public interface CalculadoraFrete {    double calcular(double pesoKg, double distanciaKm);}public final class FreteEconomico implements CalculadoraFrete {    @Override public double calcular(double pesoKg, double distanciaKm) {        return 5.0 + (0.5 * pesoKg) + (0.1 * distanciaKm);    }}public final class FreteExpresso implements CalculadoraFrete {    @Override public double calcular(double pesoKg, double distanciaKm) {        return 15.0 + (1.2 * pesoKg) + (0.25 * distanciaKm);    }}public final class CheckoutService {    public double cotar(TipoFrete tipo, double pesoKg, double distanciaKm) {        CalculadoraFrete calc;        if (tipo == TipoFrete.ECONOMICO) {            calc = new FreteEconomico();        } else {            calc = new FreteExpresso();        }        return calc.calcular(pesoKg, distanciaKm);    }    public double recotar(TipoFrete tipo, double pesoKg, double distanciaKm) {        // repetição do mesmo if/else em outro método        CalculadoraFrete calc;        if (tipo == TipoFrete.ECONOMICO) {            calc = new FreteEconomico();        } else {            calc = new FreteExpresso();        }        return calc.calcular(pesoKg, distanciaKm);    }}

Tarefas

  • Crie uma factory bem nomeada (ex.: CalculadoraFreteFactory ou FreteCalculadoraFactory) com um método criar(TipoFrete tipo).
  • Remova o if/else duplicado do CheckoutService, usando a factory.
  • Adicione um novo tipo RETIRADA_NA_LOJA que sempre retorna 0 e implemente a variação apenas adicionando uma nova classe e ajustando a factory.
  • Escreva um teste simples para garantir que TipoFrete.EXPRESSO retorna uma instância de FreteExpresso (ou que calcula um valor esperado).

Dica de implementação (estrutura esperada)

public final class CalculadoraFreteFactory {    public CalculadoraFrete criar(TipoFrete tipo) {        return switch (tipo) {            case ECONOMICO -> new FreteEconomico();            case EXPRESSO -> new FreteExpresso();            // case RETIRADA_NA_LOJA -> new FreteRetiradaNaLoja();        };    }}public final class CheckoutService {    private final CalculadoraFreteFactory factory;    public CheckoutService(CalculadoraFreteFactory factory) {        this.factory = factory;    }    public double cotar(TipoFrete tipo, double pesoKg, double distanciaKm) {        return factory.criar(tipo).calcular(pesoKg, distanciaKm);    }    public double recotar(TipoFrete tipo, double pesoKg, double distanciaKm) {        return factory.criar(tipo).calcular(pesoKg, distanciaKm);    }}

Agora responda o exercício sobre o conteúdo:

Ao refatorar um serviço que repete if/else para instanciar CalculadoraFrete (Econômico/Expresso), qual abordagem aplica corretamente o padrão Factory para reduzir acoplamento e duplicação?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A factory centraliza a criação e o mapeamento de TipoFrete para implementações, removendo if/else duplicado e fazendo o cliente depender de abstrações, não de classes concretas.

Próximo capitúlo

Padrão Strategy em Java OOP: comportamento intercambiável e eliminação de condicionais

Arrow Right Icon
Capa do Ebook gratuito Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos
82%

Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.