1. O que são Padrões de Projeto?
Padrões de Projeto (Design Patterns) são soluções reutilizáveis para problemas recorrentes no desenvolvimento de software. Eles não são códigos prontos para copiar e colar, mas sim modelos conceituais que descrevem como organizar classes e objetos para resolver um problema de forma elegante.
Por que usar padrões?
- Código mais fácil de manter — quem lê o código reconhece a estrutura e entende mais rápido a intenção do desenvolvedor.
- Vocabulário comum — deixar claro "aqui usei um Observer" comunica a estrutura inteira sem precisar desenhar tudo do zero.
- Menor acoplamento — os padrões promovem que partes do sistema se conheçam o mínimo necessário, facilitando mudanças.
- Reuso de boas soluções — são fruto de experiência acumulada de dezenas de projetos reais; você não precisa "reinventar a roda".
❌ Sem padrão
Código espalhado, classes com múltiplas responsabilidades, criação de objetos distribuída por todo o sistema, difícil de testar e modificar.
✅ Com padrão
Responsabilidades bem definidas, pontos de extensão claros, código previsível e testável, comunicação e entendimento facilitados entre os envolvidos
2. Origem — o "Gang of Four"
O termo vem do livro Design Patterns: Elements of Reusable Object-Oriented Software, publicado em 1994 pelos quatro autores Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides — apelidados de Gang of Four (GoF) - "Gangue dos Quatro".
O arquiteto Christopher Alexander publica o primeiro catálogo de padrões, voltado para construção civil — inspiração direta para os padrões de software.
Kent Beck e Ward Cunningham apresentam os primeiros padrões para software em Smalltalk na conferência OOPSLA.
Gamma, Helm, Johnson e Vlissides publicam o livro GoF com 23 padrões catalogados para linguagens orientadas a objetos.
Os 23 padrões GoF são referência e estão presentes na arquitetura de frameworks modernos como Spring (Java), Laravel (PHP), React, Angular e Vue.js (JavaScript), além de bibliotecas como Redux e Express.js.
Laravel e os Padrões GoF
O framework Laravel (PHP) é um exemplo prático de como os padrões GoF aparecem no desenvolvimento web moderno. Taylor Otwell, criador do Laravel, aplicou diversos desses padrões na arquitetura do framework:
- Singleton — a instância da aplicação Laravel garante um único container de dependências durante toda a requisição.
- Factory — usado em Model Factories (testes), gerenciadores de sessão e drivers de banco de dados.
-
Facade — as facades do Laravel (
Cache,Auth,Log) simplificam o acesso a subsistemas complexos. - Observer — Model Observers do Eloquent permitem reagir a eventos de criação, atualização e deleção de registros.
- Strategy — presente em controllers, middleware e drivers de cache/sessão, permitindo trocar comportamentos sem alterar o código cliente.
- Command — o Artisan CLI implementa este padrão, transformando operações em objetos reutilizáveis e parametrizáveis.
- Decorator — o pipeline de Middleware adiciona comportamentos (autenticação, CORS, logs) dinamicamente às requisições HTTP.
- Iterator — Collections do Laravel fornecem iteração fluente sobre conjuntos de dados.
JavaScript, React, Angular, Node.js e os Padrões GoF
O ecossistema JavaScript aplica extensivamente os padrões GoF, tanto no lado do cliente (navegador) quanto no servidor (Node.js). Frameworks modernos como React, Angular e Vue.js incorporam esses padrões em sua arquitetura:
-
Singleton — Redux/Vuex usam uma única store global de estado; módulos
do Node.js são singletons por padrão (cache do
require()). -
Observer — a base de sistemas reativos:
addEventListenerno DOM, RxJS (Angular), hooksuseEffect(React), Vuex watchers. - Factory — React.createElement, componentes factory no Angular, criação dinâmica de elementos no Vue.
- Strategy — Context API (React), estratégias de validação de formulários, múltiplos algoritmos de ordenação/filtro em listas.
-
Decorator — Higher-Order Components (HOC) no React, decorators
do Angular (
@Component,@Injectable), middlewares no Express.js. - Facade — bibliotecas como Axios e jQuery simplificam APIs complexas (HTTP, manipulação DOM); Angular Services encapsulam lógica de negócio.
- Proxy — Vue 3 usa Proxy nativo para reatividade; Immer.js para imutabilidade; lazy loading de módulos no Angular/React.
-
Iterator — métodos nativos
map(),filter(),reduce(); generators (function*); iteradores customizados. -
Module Pattern — ES6 modules (
import/export), CommonJS no Node.js, encapsulamento de código em IIFE. - Middleware Pattern — Express.js, Koa, Redux middleware para interceptar requisições e ações.
As 3 categorias dos padrões GoF
Os 23 padrões estão organizados em três grandes grupos, conforme o tipo de problema que resolvem:
Criacionais
Tratam como objetos são criados. Permitem que o código não dependa diretamente de qual classe concreta está sendo instanciada.
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
Estruturais
Tratam como classes e objetos se combinam para formar estruturas maiores e mais flexíveis.
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
Comportamentais
Tratam como objetos se comunicam e distribuem responsabilidades entre si.
- Observer
- Strategy
- Template Method
- Command
- Iterator
- State
- Chain of Responsibility
- Mediator
- Memento
- Visitor
- Interpreter
Como memorizar as categorias:
- Criacional → "Como é criado/nasce?"
-
Encapsula e controla a forma de criar objetos, reduzindo dependências do
new. - Estrutural → "Como se encaixa/estrutura?"
- Define composições de classes e objetos para montar estruturas mais complexas com flexibilidade.
- Comportamental → "Como se comporta/conversa?"
- Organiza a troca de mensagens e a distribuição de responsabilidades entre objetos em tempo de execução.
Catálogo dos 23 padrões
A tabela a seguir resume os padrões GoF descrevendo o problema que cada um resolve.
| Categoria | Padrão | Problema que resolve |
|---|---|---|
| Criacional | Singleton | Garantir que uma classe tenha apenas uma instância em todo o sistema. |
| Criacional | Factory Method | Deixar que subclasses decidam qual objeto concreto criar. |
| Criacional | Abstract Factory | Criar famílias de objetos relacionados sem especificar suas classes concretas. |
| Criacional | Builder | Construir objetos complexos passo a passo, separando construção de representação. |
| Criacional | Prototype | Criar novos objetos copiando (clonando) um existente. |
| Estrutural | Adapter | Fazer duas interfaces incompatíveis trabalharem juntas. |
| Estrutural | Bridge | Separar abstração de implementação para que ambas possam variar independentemente. |
| Estrutural | Composite | Tratar objetos individuais e coleções de objetos de forma uniforme. |
| Estrutural | Decorator | Adicionar comportamentos a objetos dinamicamente sem alterar a classe. |
| Estrutural | Facade | Prover uma interface simplificada para um subsistema complexo. |
| Estrutural | Flyweight | Compartilhar objetos para suportar grandes quantidades com uso eficiente de memória. |
| Estrutural | Proxy | Prover um substituto para controlar o acesso a outro objeto. |
| Comportamental | Observer | Notificar múltiplos objetos sobre mudanças de estado de forma desacoplada. |
| Comportamental | Strategy | Definir uma família de algoritmos e torná-los intercambiáveis. |
| Comportamental | Template Method | Definir o esqueleto de um algoritmo na superclasse, deixando passos para subclasses. |
| Comportamental | Command | Encapsular uma requisição como objeto, permitindo desfazer, enfileirar e registrar ações. |
| Comportamental | Iterator | Percorrer elementos de uma coleção sem expor sua estrutura interna. |
| Comportamental | State | Alterar o comportamento de um objeto quando seu estado interno muda. |
| Comportamental | Chain of Responsibility | Passar uma requisição por uma cadeia de processadores até que um a trate. |
| Comportamental | Mediator | Centralizar a comunicação entre objetos, reduzindo dependências diretas. |
| Comportamental | Memento | Capturar e restaurar o estado interno de um objeto sem violar encapsulamento. |
| Comportamental | Visitor | Separar algoritmos dos objetos sobre os quais operam. |
| Comportamental | Interpreter | Definir uma gramática e um interpretador para uma linguagem simples. |
Quando usar (ou não)
Use padrões quando…
- O mesmo problema (ou algo muito parecido) aparece em diferentes partes do sistema.
- Você percebe que alterar uma parte obriga a alterar muitas outras (alto acoplamento).
- Precisa adicionar comportamentos sem reescrever classes existentes.
- A equipe precisa de uma linguagem comum para descrever a arquitetura.
Evite padrões quando…
- O código é simples e funciona bem — adicionar um padrão aumentaria a complexidade sem ganho real.
- Você está aprendendo e ainda não identificou claramente o problema que o padrão resolve.
- A motivação é "parecer mais profissional" — padrões mal aplicados prejudicam legibilidade.
6. Primeiros exemplos em JavaScript
Os exemplos abaixo são introdutórios: o objetivo é ver a ideia funcionando em código. Nesta aula vamos focar em cinco padrões muito presentes no desenvolvimento web: Singleton, Observer, EventEmitter, Factory Method e Strategy.
Criacional Singleton — instância única
Garante que exista apenas um objeto de determinada classe em todo o sistema.
// Padrão Singleton — uma única instância compartilhada
class ConfiguracaoApp {
static #instancia = null;
#configuracoes;
constructor() {
this.#configuracoes = {
idioma: 'pt-BR',
tema: 'claro',
apiUrl: 'https://api.exemplo.com',
};
}
static obterInstancia() {
if (!ConfiguracaoApp.#instancia) {
ConfiguracaoApp.#instancia = new ConfiguracaoApp();
}
return ConfiguracaoApp.#instancia;
}
obter(chave) {
return this.#configuracoes[chave];
}
definir(chave, valor) {
this.#configuracoes[chave] = valor;
}
}
// Uso
const config1 = ConfiguracaoApp.obterInstancia();
const config2 = ConfiguracaoApp.obterInstancia();
config1.definir('tema', 'escuro');
console.log(config2.obter('tema')); // 'escuro' — é o mesmo objeto
console.log(config1 === config2); // true
static #instancia = null- Guarda a única instância como campo privado estático da própria classe, inacessível externamente.
static obterInstancia()- Ponto de acesso global: cria a instância na primeira chamada; nas seguintes, devolve a mesma que foi criada.
- Por que usar?
- Configurações, conexões com banco de dados, loggers — qualquer recurso que deve ser compartilhado e único em todo o sistema.
Comportamental Observer — notificar sem acoplamento
Um objeto (sujeito) mantém uma lista de dependentes (observadores) e os notifica automaticamente quando seu estado muda.
// Padrão Observer — notificação desacoplada
class EventoCarrinho {
#observadores = [];
assinar(observador) {
if (typeof observador.atualizar !== 'function') {
throw new Error('Observador deve implementar o método atualizar().');
}
this.#observadores.push(observador);
}
cancelarAssinatura(observador) {
this.#observadores = this.#observadores.filter(o => o !== observador);
}
notificar(dados) {
this.#observadores.forEach(o => o.atualizar(dados));
}
}
class Carrinho extends EventoCarrinho {
#itens = [];
adicionarItem(item) {
this.#itens.push(item);
this.notificar({ evento: 'item-adicionado', item, total: this.#itens.length });
}
}
// Observadores concretos
const contadorUI = {
atualizar({ total }) {
console.log(`[UI] Carrinho atualizado: ${total} item(s).`);
},
};
const logServidor = {
atualizar({ evento, item }) {
console.log(`[Log] Evento "${evento}" registrado para o item "${item}".`);
},
};
// Uso
const carrinho = new Carrinho();
carrinho.assinar(contadorUI);
carrinho.assinar(logServidor);
carrinho.adicionarItem('Mouse sem fio');
// [UI] Carrinho atualizado: 1 item(s).
// [Log] Evento "item-adicionado" registrado para o item "Mouse sem fio".
assinar(observador)/cancelarAssinatura(observador)- Permitem que qualquer objeto interessado entre ou saia da lista de notificados — sem que o carrinho precise conhecer os detalhes de quem está ouvindo.
notificar(dados)-
Percorre todos os observadores e chama
atualizar()em cada um, passando os dados do evento. - Onde aparece no desenvolvimento web?
-
Eventos do DOM (
addEventListener), stores de estado (Redux, Vuex), WebSockets, sistemas de notificação em tempo real — todos usam a ideia do Observer.
Comportamental EventEmitter — eventos nomeados
Um EventEmitter é uma implementação concreta da ideia do Observer: você
registra ouvintes para eventos identificados por nome e emite esses eventos passando
dados para todos os ouvintes inscritos.
// Implementação simples de EventEmitter em JavaScript
class EventEmitter {
#eventos = {};
on(evento, listener) {
if (!this.#eventos[evento]) {
this.#eventos[evento] = [];
}
this.#eventos[evento].push(listener);
}
off(evento, listener) {
if (!this.#eventos[evento]) return;
this.#eventos[evento] = this.#eventos[evento].filter(l => l !== listener);
}
emit(evento, dados) {
if (!this.#eventos[evento]) return;
this.#eventos[evento].forEach(listener => listener(dados));
}
}
// Uso — simulando notificações em uma aplicação web
const notificacoes = new EventEmitter();
function mostrarToast(mensagem) {
console.log('[TOAST]', mensagem);
}
function registrarLog(mensagem) {
console.log('[LOG]', mensagem);
}
notificacoes.on('usuario:login', mostrarToast);
notificacoes.on('usuario:login', registrarLog);
notificacoes.emit('usuario:login', 'Usuário entrou no sistema');
// [TOAST] Usuário entrou no sistema
// [LOG] Usuário entrou no sistema
on(evento, listener)- Registra uma função para ser chamada sempre que o evento com esse nome for emitido.
emit(evento, dados)- Dispara o evento e chama todos os listeners cadastrados, passando os dados necessários.
- Onde isso aparece?
-
APIs do Node.js, bibliotecas de front-end e sistemas de eventos em geral usam alguma
forma de
EventEmitterpor baixo.
Criacional Factory Method — escolhendo o tipo certo
O Factory Method encapsula a lógica de criação de objetos em um único ponto,
permitindo que você devolva diferentes implementações da mesma interface sem espalhar
new e if/switch pelo código.
// Padrão Factory Method — escolhendo o tipo de notificador
class Notificador {
enviar(mensagem) {
throw new Error('Método enviar() deve ser implementado.');
}
}
class NotificadorEmail extends Notificador {
enviar(mensagem) {
console.log(`Enviando e-mail: ${mensagem}`);
}
}
class NotificadorSMS extends Notificador {
enviar(mensagem) {
console.log(`Enviando SMS: ${mensagem}`);
}
}
class NotificadorPush extends Notificador {
enviar(mensagem) {
console.log(`Enviando notificação push: ${mensagem}`);
}
}
// Creator com o Factory Method
class NotificadorFactory {
static criar(tipo) {
switch (tipo) {
case 'email': return new NotificadorEmail();
case 'sms': return new NotificadorSMS();
case 'push': return new NotificadorPush();
default:
throw new Error(`Tipo de notificador desconhecido: ${tipo}`);
}
}
}
// Uso — o código cliente só conhece a interface Notificador
const notificador = NotificadorFactory.criar('email');
notificador.enviar('Bem-vindo à plataforma!');
NotificadorFactory.criar(tipo)-
Centraliza a decisão de qual classe concreta instanciar, devolvendo sempre algo que
se comporta como
Notificador. - Por que isso ajuda?
- Se surgir um novo tipo de notificação (por exemplo, WhatsApp), você adiciona uma classe e ajusta a fábrica, sem mudar o código que consome o notificador.
Comportamental Strategy — algoritmos intercambiáveis
Define uma família de algoritmos e permite trocá-los sem alterar o código que os utiliza.
// Padrão Strategy — forma de pagamento intercambiável
class ProcessadorPagamento {
#estrategia;
constructor(estrategia) {
this.#estrategia = estrategia;
}
definirEstrategia(estrategia) {
this.#estrategia = estrategia;
}
processar(valor) {
return this.#estrategia.executar(valor);
}
}
// Estratégias concretas
const pagamentoCartao = {
executar(valor) {
return `Pagamento de R$ ${valor.toFixed(2)} realizado via Cartão de Crédito.`;
},
};
const pagamentoPix = {
executar(valor) {
return `Pagamento de R$ ${valor.toFixed(2)} realizado via Pix (instantâneo).`;
},
};
const pagamentoBoleto = {
executar(valor) {
return `Boleto de R$ ${valor.toFixed(2)} gerado. Vencimento em 3 dias úteis.`;
},
};
// Uso — troca de estratégia em tempo de execução
const processador = new ProcessadorPagamento(pagamentoPix);
console.log(processador.processar(150));
processador.definirEstrategia(pagamentoCartao);
console.log(processador.processar(150));
this.#estrategia = estrategia- O processador guarda uma referência à estratégia ativa, sem depender de qual classe concreta ela é.
definirEstrategia()-
Permite trocar o comportamento em tempo de execução — sem condicional
if/elseespalhado pelo código. - Por que isso importa para web?
- Gateways de pagamento, validações de formulário, algoritmos de ordenação, formatação de respostas de API — todos são candidatos naturais ao Strategy.