Programação II – ECO Padrões de Projeto Introdução

Introdução a Padrões de Projeto (GoF)

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

Atenção: Aplicar um padrão onde ele não é necessário aumenta a complexidade sem benefício. Aprenda reconhecer o problema que cada padrão resolve.

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".

Década de 1970

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.

1987

Kent Beck e Ward Cunningham apresentam os primeiros padrões para software em Smalltalk na conferência OOPSLA.

1994

Gamma, Helm, Johnson e Vlissides publicam o livro GoF com 23 padrões catalogados para linguagens orientadas a objetos.

Hoje

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.

O livro GoF foi escrito com exemplos em C++ e Smalltalk, mas os padrões se aplicam a qualquer linguagem orientada a objetos — inclusive JavaScript e PHP.
🐘

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: addEventListener no DOM, RxJS (Angular), hooks useEffect (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.

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.
Regra prática para começar: primeiro faça funcionar, depois refatore identificando padrões. É mais natural reconhecer um padrão em código existente do que tentar aplicá-lo antes de entender o problema.

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 EventEmitter por 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/else espalhado 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.