SkillsTecnológicas
Menu
Back-end

Padrões de Projeto: 5 Design Patterns com Exemplos

Aprenda cinco padrões de projeto com exemplos em Java: Factory, Builder, Adapter, Decorator e Strategy, além de critérios para escolher cada um.

Marcos RodriguesPublicado em 21 de abril de 2024Atualizado em 23 de agosto de 202612 min de leitura
Arquiteto de software organiza cinco módulos que representam diferentes padrões de projeto

Padrões de projeto, ou design patterns, são soluções reutilizáveis para problemas de design que aparecem repetidamente no desenvolvimento de software. Eles não são bibliotecas prontas nem regras obrigatórias: são modelos de colaboração entre classes e objetos que ajudam uma equipe a discutir alternativas com precisão.

Neste guia, você aprenderá cinco padrões com exemplos em Java: Factory, Builder, Adapter, Decorator e Strategy. Eles cobrem criação, estrutura e comportamento e mostram quando uma abstração reduz acoplamento — e quando apenas acrescenta complexidade.

Para aproveitar os exemplos, é importante entender interfaces, composição e polimorfismo. Se necessário, consulte primeiro o guia de orientação a objetos em Java.

O que são padrões de projeto?

Um padrão de projeto descreve um problema recorrente, as forças envolvidas e uma estrutura de solução. Em vez de entregar uma classe para copiar, ele oferece um vocabulário: quando alguém propõe “usar Strategy”, a equipe entende que um algoritmo variável será isolado atrás de um contrato e fornecido ao contexto.

Os padrões mais conhecidos foram organizados no livro Design Patterns: Elements of Reusable Object-Oriented Software, do chamado Gang of Four. As ideias continuam úteis, mas a implementação muda conforme a linguagem. Lambdas, records, injeção de dependência e APIs modernas podem reduzir bastante a cerimônia necessária.

O valor está na decisão, não no nome. Se duas linhas simples resolvem o problema de forma clara, não é preciso criar cinco interfaces apenas para afirmar que existe um padrão.

As três categorias dos design patterns

CategoriaProblema principalExemplos
CriacionaisComo objetos são criados sem acoplar o consumidor à construção concretaFactory, Builder, Abstract Factory
EstruturaisComo classes e objetos são combinados para formar estruturas flexíveisAdapter, Decorator, Facade
ComportamentaisComo responsabilidades, algoritmos e mensagens circulam entre objetosStrategy, Observer, Command

As categorias ajudam a localizar o problema. Elas não impedem combinações: uma Factory pode criar um Decorator que recebe uma Strategy. O importante é que cada padrão tenha uma razão observável para existir.

Como saber se um padrão é necessário

Comece pelo ponto de mudança. Existem várias implementações? A construção tem combinações inválidas? Uma API externa possui contrato incompatível? O comportamento precisa ser acrescentado sem multiplicar subclasses? O algoritmo muda por cliente ou contexto?

  • Nomeie o problema antes do padrão.
  • Implemente a solução simples e identifique a duplicação ou acoplamento real.
  • Defina o que precisa variar e o que deve permanecer estável.
  • Compare o custo da abstração com a frequência esperada de mudança.
  • Registre a decisão para que a equipe saiba por que a estrutura existe.

Um padrão é justificável quando torna uma mudança provável mais localizada. Abstrair mudanças imaginárias costuma produzir código genérico demais e difícil de navegar.

1. Factory: centralizar a escolha da implementação

Factory encapsula a criação de objetos. O consumidor pede um produto por um contrato comum e não precisa conhecer construtores, dependências ou regras de seleção de cada classe concreta.

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

final class NotificadorFactory {
    static Notificador criar(String canal) {
        return switch (canal.toLowerCase()) {
            case "email" -> new NotificadorEmail();
            case "sms" -> new NotificadorSms();
            default -> throw new IllegalArgumentException("Canal inválido");
        };
    }
}

Notificador notificador = NotificadorFactory.criar("email");
notificador.enviar("ana@exemplo.com", "Pedido aprovado");

Esse exemplo é uma fábrica simples. No Factory Method clássico, uma superclasse declara o método de criação e subclasses decidem o produto concreto. Em Java, uma fábrica também pode receber um Supplier oficial da API, evitando condicionais centrais quando os fornecedores são registrados em um mapa.

Quando usar Factory

  • O consumidor deve depender de uma interface, não de classes concretas.
  • A criação exige dependências, configuração ou validação próprias.
  • A implementação é escolhida por canal, formato, ambiente ou provedor.
  • Você quer concentrar a política de criação em um ponto testável.

Evite Factory para uma classe simples que só possui um construtor óbvio e não varia. Uma fábrica que apenas chama new Produto() sem proteger qualquer decisão acrescenta uma camada sem benefício.

2. Builder: construir objetos por etapas

Builder separa a construção de um objeto complexo de sua representação final. Ele é útil quando existem muitos parâmetros opcionais, etapas claras ou combinações que precisam ser validadas antes de produzir a instância.

Pedido pedido = Pedido.builder("cliente-42")
    .adicionarItem("teclado", 1)
    .adicionarItem("mouse", 2)
    .cupom("DEV10")
    .entregaExpressa(true)
    .build();

O método build() é a fronteira: valida campos obrigatórios, impede quantidade negativa e devolve um objeto pronto. Isso é melhor do que um construtor com muitos booleanos, como new Pedido(id, true, false, null, ...), cuja intenção é difícil de ler.

A própria plataforma usa a ideia de construção incremental em classes como StringBuilder. Para uma implementação completa com classe interna e validação, consulte o artigo sobre o padrão Builder em Java.

Quando usar Builder

  • O construtor teria vários parâmetros opcionais do mesmo tipo.
  • A construção ocorre em etapas e precisa ser legível.
  • O resultado deve ser imutável depois de criado.
  • Existem representações diferentes produzidas pelo mesmo processo.

Não use Builder para mascarar uma classe com responsabilidades demais. Se o objeto possui trinta propriedades sem relação coesa, talvez o problema seja a modelagem, não o construtor.

Estação modular representa Factory, Builder, Adapter e Decorator em uma arquitetura de software
Factory e Builder controlam a criação; Adapter compatibiliza contratos; Decorator adiciona responsabilidades por composição.

3. Adapter: compatibilizar contratos

Adapter converte a interface de um componente para o contrato esperado pelo cliente. Ele é comum em integrações com SDKs, APIs legadas e bibliotecas de terceiros. A regra externa fica confinada ao adaptador, enquanto o domínio usa sua própria linguagem.

interface GatewayPagamento {
    String cobrar(double valorEmReais);
}

final class AdaptadorGatewayLegado implements GatewayPagamento {
    private final ClienteLegado cliente;

    AdaptadorGatewayLegado(ClienteLegado cliente) {
        this.cliente = cliente;
    }

    @Override
    public String cobrar(double valorEmReais) {
        long centavos = Math.round(valorEmReais * 100);
        RespostaLegada resposta = cliente.makeCharge(centavos, "BRL");
        return resposta.transactionId();
    }
}

O restante da aplicação não conhece centavos, nomes em inglês ou o tipo de resposta do fornecedor. Um exemplo na biblioteca padrão é InputStreamReader, que funciona como ponte entre fluxos de bytes e fluxos de caracteres.

Quando usar Adapter

  • Uma dependência útil possui interface incompatível com o domínio.
  • Você está substituindo gradualmente um sistema legado.
  • Vários fornecedores precisam cumprir o mesmo contrato interno.
  • Conversões de unidade, formato ou erro não devem se espalhar pelo sistema.

O adaptador deve traduzir, não esconder toda a lógica de negócio. Se ele começa a decidir descontos, estoque e autorização, está acumulando responsabilidades que pertencem a serviços ou entidades do domínio.

4. Decorator: adicionar comportamento por composição

Decorator envolve um objeto que implementa um contrato e preserva esse mesmo contrato, acrescentando comportamento antes ou depois da delegação. Diferentes decoradores podem ser combinados sem criar uma subclasse para cada combinação.

interface RepositorioUsuario {
    Usuario buscar(long id);
}

final class RepositorioComCache implements RepositorioUsuario {
    private final RepositorioUsuario origem;
    private final Map<Long, Usuario> cache = new HashMap<>();

    RepositorioComCache(RepositorioUsuario origem) {
        this.origem = origem;
    }

    @Override
    public Usuario buscar(long id) {
        return cache.computeIfAbsent(id, origem::buscar);
    }
}

É possível envolver o repositório com cache, métricas e auditoria em ordens diferentes. A API de I/O do Java oferece uma referência clássica em FilterInputStream, base para fluxos que processam outro fluxo. Veja também a explicação detalhada do padrão Decorator.

Quando usar Decorator

  • Responsabilidades opcionais precisam ser combinadas dinamicamente.
  • A herança produziria muitas subclasses para todas as combinações.
  • O comportamento adicional respeita exatamente o contrato original.
  • Preocupações como cache, métricas ou retry devem ficar fora da regra principal.

Uma cadeia longa pode dificultar o diagnóstico. Mantenha decoradores pequenos, defina uma ordem previsível e exponha métricas que permitam entender onde uma chamada falhou.

5. Strategy: trocar algoritmos em tempo de execução

Strategy encapsula algoritmos intercambiáveis atrás de um contrato. O contexto recebe a estratégia e delega a ela uma decisão, evitando condicionais que crescem sempre que surge uma nova regra.

@FunctionalInterface
interface CalculadoraFrete {
    BigDecimal calcular(Pedido pedido);
}

final class Checkout {
    private final CalculadoraFrete frete;

    Checkout(CalculadoraFrete frete) {
        this.frete = frete;
    }

    BigDecimal total(Pedido pedido) {
        return pedido.subtotal().add(frete.calcular(pedido));
    }
}

CalculadoraFrete retirada = pedido -> BigDecimal.ZERO;
Checkout checkout = new Checkout(retirada);

Interfaces funcionais tornam estratégias simples bastante compactas. A interface oficial Comparator é um exemplo familiar: o algoritmo de ordenação recebe uma estratégia de comparação sem precisar conhecer a regra concreta.

Mecanismo escolhe rotas intercambiáveis e distribui eventos para módulos observadores
Strategy troca o algoritmo usado pelo contexto; Observer distribui notificações para interessados sem acoplar o emissor a cada ação concreta.

Quando usar Strategy

  • Existem algoritmos alternativos para a mesma responsabilidade.
  • A regra varia por cliente, plano, região ou configuração.
  • Condicionais de tipo aparecem em vários pontos.
  • Cada algoritmo precisa de testes independentes.

Se há apenas uma regra estável, extraí-la para uma interface pode ser prematuro. Comece simples e introduza Strategy quando surgir uma segunda implementação legítima ou uma fronteira externa que precise ser isolada.

Observer como padrão complementar

Observer cria uma relação de assinatura: quando o sujeito muda ou publica um evento, observadores registrados são notificados. Ele serve para propagar acontecimentos sem o emissor chamar diretamente cada ação concreta.

Uma compra aprovada pode disparar e-mail, atualização de pontos e métricas. O evento não deve, porém, esconder etapas que precisam fazer parte da mesma transação. A API PropertyChangeSupport demonstra o registro e a notificação de listeners. Em sistemas distribuídos, filas e brokers ampliam a ideia, adicionando desafios como repetição, ordem e consistência.

Como os cinco padrões trabalham juntos

Imagine um checkout que aceita vários provedores. Uma Factory seleciona o gateway; um Adapter traduz o SDK externo; Decorators acrescentam métricas e proteção contra repetição; uma Strategy calcula frete; e um Builder monta a configuração imutável do checkout.

Essa combinação só é saudável quando cada camada resolve um problema confirmado. Para uma API pequena com um único provedor, começar com todas elas seria exagero. Em um sistema que integra vários meios de pagamento, diferentes regras e requisitos operacionais, as fronteiras podem compensar. O guia de desenvolvimento de APIs ajuda a conectar essas decisões à arquitetura de uma aplicação real.

Comparativo rápido dos cinco padrões

PadrãoPergunta que respondeSinal típicoRisco
FactoryQuem escolhe e cria a implementação?new e seleção espalhadosFábrica central gigante
BuilderComo montar um objeto complexo?Construtor telescópicoEsconder modelo inchado
AdapterComo integrar um contrato incompatível?Tipos externos vazandoAcumular regra de negócio
DecoratorComo acrescentar comportamento combinável?Muitas subclassesCadeia difícil de rastrear
StrategyComo trocar um algoritmo?Condicionais por tipoInterface para uma única regra

Padrão de projeto não é arquitetura

Um design pattern resolve uma colaboração localizada entre objetos. Arquitetura define fronteiras maiores: módulos, fluxos de dados, deploy, persistência e comunicação entre sistemas. MVC, MVP e MVVM são padrões arquiteturais de apresentação, não substitutos de Factory ou Strategy.

Leia o comparativo de MVC, MVP e MVVM para perceber a diferença de escala. Em uma mesma aplicação MVC, serviços internos podem usar Adapter, Decorator e Strategy.

Erros comuns ao aplicar design patterns

  • Escolher pelo nome: começar com “quero usar Observer” em vez de descrever o problema.
  • Abstrair antes da variação: criar contratos sem uma segunda implementação plausível.
  • Copiar diagramas literalmente: ignorar recursos da linguagem que simplificam o padrão.
  • Confundir Factory e Builder: Factory escolhe/cria um produto; Builder conduz etapas de montagem.
  • Transformar Adapter em serviço universal: misturar tradução externa com regras do domínio.
  • Usar herança em Decorator: perder a capacidade de combinar responsabilidades em execução.
  • Encadear sem observabilidade: tornar difícil descobrir qual decorador ou listener falhou.
  • Não remover o padrão: manter uma abstração mesmo quando a variação deixou de existir.

Padrões são ferramentas de manutenção. Se uma estrutura deixou de justificar seu custo, simplificá-la é uma melhoria, não um retrocesso.

Como testar classes que usam padrões

  • Factory: verifique a implementação produzida para cada entrada válida e o erro para entradas desconhecidas.
  • Builder: teste campos obrigatórios, combinações inválidas e imutabilidade do resultado.
  • Adapter: teste conversões de unidade, mapeamento de erros e respostas do fornecedor.
  • Decorator: confirme o comportamento adicional e se a delegação acontece uma única vez.
  • Strategy: exercite cada algoritmo isoladamente e o contexto com uma estratégia falsa.

O teste deve observar contratos públicos, não a quantidade de classes. Para exemplos com doubles, spies e testes parametrizados, veja o guia de JUnit e Mockito.

Checklist antes de escolher um padrão

  • Qual mudança concreta está cara hoje?
  • Quais partes variam e qual contrato permanece estável?
  • Uma função, composição simples ou record resolveria com menos peças?
  • A equipe reconhecerá o padrão ou a decisão precisa ser documentada?
  • É possível testar o comportamento sem acessar detalhes internos?
  • O padrão reduz condicionais e duplicação ou apenas as desloca?
  • Logs e métricas continuam permitindo rastrear o fluxo?
  • Existe um caminho claro para simplificar caso a variação desapareça?

Exercícios para praticar

  1. Crie uma Factory que escolha exportadores JSON, CSV e XML por uma enumeração.
  2. Modele um Builder para uma requisição HTTP com cabeçalhos, timeout e corpo opcionais.
  3. Adapte um cliente externo que trabalha com centavos para um contrato interno que usa BigDecimal.
  4. Decore um repositório com cache e métricas, testando a ordem das chamadas.
  5. Implemente três estratégias de desconto sem condicionais dentro do checkout.
  6. Compare a solução com uma versão direta e explique quando a abstração passa a compensar.

Você pode aplicar os exercícios em um projeto de pedidos e persistir os dados com um repositório. O artigo sobre o padrão DAO mostra como isolar o acesso a dados sem misturá-lo à regra de negócio.

Perguntas frequentes

Preciso decorar todos os padrões?

Não. Aprenda a reconhecer problemas de criação, composição e variação de comportamento. Consulte referências quando necessário e pratique alguns padrões frequentes até entender seus custos.

Design patterns servem apenas para Java?

Não. Eles nasceram no contexto orientado a objetos, mas aparecem em várias linguagens. A implementação muda: funções de primeira classe tornam Strategy simples; objetos literais podem reduzir Builders; sistemas de módulos mudam a forma de criar Factories.

Qual padrão devo aprender primeiro?

Strategy é um bom começo porque demonstra composição, interfaces e substituição com pouco código. Depois, estude Factory e Adapter, comuns em integrações, e avance para Builder e Decorator.

Padrões sempre deixam o código melhor?

Não. Aplicados sem necessidade, aumentam arquivos, indireção e custo cognitivo. Eles melhoram o design quando respondem a uma variação real e deixam a próxima mudança relevante mais segura.

Conclusão

Factory centraliza a criação, Builder organiza montagens complexas, Adapter traduz contratos, Decorator combina responsabilidades e Strategy substitui algoritmos. Juntos, esses cinco padrões formam uma base prática para reconhecer problemas recorrentes sem transformar o sistema em uma coleção de abstrações desnecessárias.

O melhor caminho é começar pelo código mais simples, observar onde a mudança se repete e introduzir o padrão com uma justificativa concreta. Quando a equipe consegue explicar o problema, o contrato está coeso e os testes comprovam substituição, o padrão cumpre sua função: tornar a evolução mais localizada e previsível.