Padrões de design na prática
Os design patterns que você vai encontrar no código de times reais, sem decorar o livro do GoF: o que são, quando aplicar, e quando não aplicar.
Design patterns são soluções reutilizáveis para problemas comuns de software. O livro clássico do GoF (Gang of Four) tem 23 padrões catalogados, e muitos devs se sentem obrigados a decorar todos. Na prática, você vai usar meia dúzia com frequência e o resto raramente. Este guia cobre os que aparecem de verdade no código de times reais, com exemplos reconhecíveis.
Uma observação honesta antes de começar: design patterns são ferramentas, não objetivos. Código simples sem padrão explícito é melhor que código complicado com padrão aplicado errado. O sinal de que um padrão se encaixa é que ele simplifica, não que complica.
Singleton: um objeto para toda a aplicação
O problema: você precisa de uma única instância de algo, conexão com banco de dados, configuração da aplicação, logger. Criar múltiplas instâncias seria desperdício ou causaria comportamento inconsistente.
A solução: a classe garante que só existe uma instância e provê um ponto global de acesso a ela.
Na prática: muitos frameworks já implementam isso implicitamente. Módulos Node.js são singletons por padrão (um módulo é carregado uma vez e cacheado). Em Python, módulos também. Em Java, Spring beans são singletons por padrão.
Quando usar: conexões de banco (pool), configuração, serviços de cache.
Quando não usar: quando você precisa de múltiplas instâncias configuradas diferente (aí use factory), ou quando dificulta testes unitários (singleton com estado global é difícil de testar isoladamente).
Observer: notificar sem acoplar
O problema: quando algo acontece (um pedido é feito, um usuário se cadastra), vários sistemas precisam ser notificados. Se você chamar cada um diretamente, você está acoplando o evento a todos os consumidores.
A solução: quem produz o evento não conhece quem consome. Consumidores se registram para receber notificações. O produtor apenas publica.
Na prática: é o padrão por trás de event emitters (Node.js EventEmitter), React state updates, Redux, RxJS, e sistemas de mensageria (Kafka, RabbitMQ são Observer em escala distribuída).
// EventEmitter do Node.js é Observer
emitter.on('pedido:criado', enviarEmail);
emitter.on('pedido:criado', atualizarEstoque);
emitter.emit('pedido:criado', { pedidoId: 123 });
// Quem emite não conhece os listeners
Quando usar: eventos de domínio, notificações, sistemas onde o número de consumidores pode crescer.
Repository: separar acesso a dados da lógica
O problema: lógica de negócio misturada com queries de banco de dados. Quando você muda de PostgreSQL para MongoDB, ou quando quer testar sem banco, você precisa reescrever tudo.
A solução: uma camada repository abstrai o acesso a dados. O resto do código chama userRepository.findById(id) sem saber se é SQL, NoSQL ou uma API.
Na prática: muito comum em back-end com TypeScript (NestJS usa repositórios com TypeORM/Prisma), Django tem querysets que encapsulam queries, Spring Data JPA gera repositórios automaticamente.
Quando usar: apps com lógica de negócio relevante, quando você quer testar sem banco (mock do repository), quando pode haver mudança de banco de dados.
Quando não usar: scripts simples, CRUDs que não têm lógica de negócio real, adiciona indireção desnecessária.
Factory: criar objetos sem especificar a classe exata
O problema: você precisa criar objetos de tipos diferentes dependendo de uma condição, mas não quer if/else espalhado pelo código.
A solução: centraliza a criação de objetos em um lugar. O código que usa o objeto não sabe qual tipo específico recebeu.
Na prática: parsers que retornam o tipo certo baseado no formato do arquivo, clients HTTP que retornam implementação real ou mock dependendo do ambiente, notificações que criam EmailNotification, PushNotification ou SMSNotification dependendo da preferência do usuário.
Quando usar: quando a criação de objetos envolve lógica, quando o tipo concreto depende de condições em runtime.
Strategy: trocar algoritmo em tempo de execução
O problema: você tem várias formas de fazer a mesma coisa (diferentes algoritmos de ordenação, diferentes métodos de pagamento, diferentes formas de calcular desconto) e quer trocar entre elas sem mudar o código que as usa.
A solução: cada algoritmo vira uma estratégia (classe ou função) com a mesma interface. O código que usa recebe a estratégia como parâmetro.
Na prática: métodos de pagamento (CartaoStrategy, PixStrategy, BoletoStrategy), processadores de arquivo (CSVParser, JSONParser, XMLParser), sorting com comparators customizados.
Quando você vê muitos if/else ou switch/case sobre um tipo para decidir comportamento, Strategy é provavelmente o padrão certo.
Como aprender mais (sem decorar o livro)
Não recomendo começar pelo livro do GoF. É denso e os exemplos são em Smalltalk/C++. O caminho mais prático:
- Leia código de projetos open source que você usa (Express, Django, Spring têm todos esses padrões implementados)
- Quando você percebe que está repetindo a mesma estrutura de código, pesquise se existe um padrão para isso
- Refactoring.Guru tem os 23 padrões com exemplos em múltiplas linguagens e é muito mais acessível que o livro original
Checklist:
- Consigo identificar pelo menos um Observer no código que uso no dia a dia (EventEmitter, useState, etc)?
- Entendo por que Repository facilita testes unitários?
- Já vi ou usei Factory para criar objetos diferentes baseados em condição?
- Reconheço quando um Strategy seria melhor do que um if/else longo?