Padrão Comportamental (GoF)

Chain of Responsibility

Encadeia handlers em uma corrente sequencial, dando a cada um a chance de processar uma requisição ou passá-la ao próximo — desacoplando quem envia a requisição de quem a processa.

Intenção

Evitar o acoplamento entre remetente e receptor de uma requisição, dando a mais de um objeto a oportunidade de tratá-la. Os handlers são encadeados; cada um pode processar a requisição e encerrar a corrente, ou passá-la ao próximo handler sem que o remetente saiba quantos handlers existem ou qual deles agirá.

Catalogado pelo GoF (1994) como padrão comportamental, o Chain of Responsibility (CoR) aparece em pipelines de middleware HTTP (autenticação → autorização → throttle → handler de rota), em fluxos de aprovação hierárquica (funcionário → gerente → diretor → CEO), em sistemas de log (debug → info → warning → error) e em filtros de eventos de UI. A corrente é montada em runtime com setNext(), o que permite alterar, reordenar ou estender os handlers sem modificar o remetente.

Problema

Imagine um sistema de aprovação de despesas corporativas. Diferentes valores exigem aprovação de diferentes níveis hierárquicos. A abordagem ingênua centraliza toda a lógica num único ponto:

// Abordagem ingênua — NÃO faça isso:
function aprovarDespesa(valor: number, aprovador: string): boolean {
  if (aprovador === 'chefe' && valor <= 1000) {
    console.log('Chefe aprovou');
    return true;
  } else if (aprovador === 'gerente' && valor <= 5000) {
    console.log('Gerente aprovou');
    return true;
  } else if (aprovador === 'diretor' && valor <= 20000) {
    console.log('Diretor aprovou');
    return true;
  }
  console.log('Rejeitado');
  return false;
}
// Problema: remetente precisa saber QUEM aprovar; adicionar um nível
// exige modificar esta função; condicionais crescem indefinidamente.

Problemas imediatos: o remetente precisa conhecer todos os aprovadores possíveis e suas regras; adicionar um novo nível de aprovação exige modificar a função central; a lógica de cada nível está misturada num único bloco; não há como reordenar ou substituir um aprovador sem alterar o código que os usa.

O Chain of Responsibility resolve isso transformando cada aprovador em um handler com a mesma interface, encadeados via setNext(). O remetente envia para o primeiro handler e não sabe — nem precisa saber — quem processará a requisição.

Solução

O Chain of Responsibility organiza o código em dois elementos centrais:

  1. Handler (interface/classe abstrata): declara o método de tratamento (ex.: handle(requisicao)) e o método de encadeamento (setNext(handler)). A classe base abstrata implementa setNext() e o repasse padrão: se não processar, delega ao próximo. O handler concreto só precisa implementar sua própria lógica de decisão.
  2. ConcreteHandler: implementa a lógica de verificação — decide se processa a requisição (e encerra a corrente) ou chama super.handle() para passar ao próximo. Ex.: ChefeHandler que aprova apenas se valor ≤ 1000.

A corrente é montada pelo cliente encadeando handlers com setNext(). Geralmente retorna this para permitir encadeamento fluente: chefe.setNext(gerente).setNext(diretor).

Estrutura

          «interface» Handler
  ┌────────────────────────────────────────┐
  │ + setNext(h: Handler): Handler         │
  │ + handle(req: Requisicao): string|null │
  └────────────────────────────────────────┘
               ▲
    HandlerBase (classe abstrata)
  ┌────────────────────────────────────────┐
  │ - proximo: Handler | null              │
  │ + setNext(h: Handler): Handler         │
  │   → this.proximo = h; return h         │
  │ + handle(req): string|null             │
  │   → this.proximo?.handle(req) ?? null  │
  └────────────────────────────────────────┘
               ▲
   ┌───────────┴────────────┬──────────────────┐
   │                        │                  │
ChefeHandler         GerenteHandler      DiretorHandler
  │ handle(req):             │ handle(req):      │ handle(req):
  │  se valor <= 1000        │  se valor <= 5000 │  se valor <= 20000
  │    → processa            │    → processa     │    → processa
  │  senão                   │  senão            │  senão
  │    → super.handle(req)   │    → super        │    → super


Montagem da corrente (cliente):

  chefe.setNext(gerente).setNext(diretor)

  Fluxo para valor = 3500:

  chefe.handle(3500)
    → 3500 > 1000 → passa para gerente
    gerente.handle(3500)
      → 3500 <= 5000 → GERENTE APROVA ✓ (corrente encerrada)

  Fluxo para valor = 50000:

  chefe → gerente → diretor → null (sem handler) → rejeitado

Exemplos de código

Exemplo 1 — Pipeline de aprovação de despesas

Implementação completa com interface Handler, classe base abstrata que encapsula o repasse e handlers concretos para cada nível de aprovação. Note o encadeamento fluente via setNext() retornando o próximo handler.

// ── Interface Handler ─────────────────────────────────────────
interface Handler {
  setNext(handler: Handler): Handler;
  handle(valor: number): string | null;
}

// ── Classe base abstrata — implementa setNext e repasse padrão ─
abstract class HandlerBase implements Handler {
  private proximo: Handler | null = null;

  setNext(handler: Handler): Handler {
    this.proximo = handler;
    return handler; // retorna o próximo → encadeamento fluente
  }

  handle(valor: number): string | null {
    // Comportamento padrão: passa ao próximo ou retorna null
    return this.proximo ? this.proximo.handle(valor) : null;
  }
}

// ── Handlers concretos ────────────────────────────────────────
class ChefeHandler extends HandlerBase {
  handle(valor: number): string | null {
    if (valor <= 1_000) {
      return `Chefe aprovou R$${valor}`;
    }
    return super.handle(valor); // passa ao próximo
  }
}

class GerenteHandler extends HandlerBase {
  handle(valor: number): string | null {
    if (valor <= 5_000) {
      return `Gerente aprovou R$${valor}`;
    }
    return super.handle(valor);
  }
}

class DiretorHandler extends HandlerBase {
  handle(valor: number): string | null {
    if (valor <= 20_000) {
      return `Diretor aprovou R$${valor}`;
    }
    return super.handle(valor);
  }
}

// Handler final: captura o que escapou da corrente
class RejeitadoHandler extends HandlerBase {
  handle(valor: number): string | null {
    return `REJEITADO — R$${valor} acima do limite de R$20.000`;
  }
}

// ── Montagem da corrente ──────────────────────────────────────
const chefe   = new ChefeHandler();
const gerente = new GerenteHandler();
const diretor = new DiretorHandler();
const fim     = new RejeitadoHandler();

// Encadeamento fluente: chefe → gerente → diretor → fim
chefe.setNext(gerente).setNext(diretor).setNext(fim);

// ── Uso ──────────────────────────────────────────────────────
const despesas = [500, 3_500, 15_000, 50_000];

for (const valor of despesas) {
  const resultado = chefe.handle(valor);
  console.log(resultado);
}
// Chefe aprovou R$500
// Gerente aprovou R$3500
// Diretor aprovou R$15000
// REJEITADO — R$50000 acima do limite de R$20.000

Exemplo 2 — Middleware HTTP: autenticação → autorização → throttle

O padrão é idêntico ao de frameworks HTTP como Express.js ou Laravel. Cada middleware decide processar (e passar ao próximo) ou interromper a corrente retornando uma resposta de erro. A corrente é montada na configuração da aplicação, independente das rotas.

// ── Tipos de requisição/resposta HTTP simplificados ───────────
interface Requisicao {
  path: string;
  token?: string;
  perfil?: 'admin' | 'usuario';
  ip: string;
}

interface Resposta {
  status: number;
  corpo: string;
}

// ── Interface do middleware ───────────────────────────────────
interface Middleware {
  setNext(m: Middleware): Middleware;
  processar(req: Requisicao): Resposta;
}

// ── Classe base abstrata ──────────────────────────────────────
abstract class MiddlewareBase implements Middleware {
  private proximo: Middleware | null = null;

  setNext(m: Middleware): Middleware {
    this.proximo = m;
    return m;
  }

  protected passarAdiante(req: Requisicao): Resposta {
    return this.proximo
      ? this.proximo.processar(req)
      : { status: 204, corpo: 'OK (sem handler final)' };
  }

  abstract processar(req: Requisicao): Resposta;
}

// ── Middleware 1: autenticação ────────────────────────────────
class AutenticacaoMiddleware extends MiddlewareBase {
  processar(req: Requisicao): Resposta {
    if (!req.token) {
      return { status: 401, corpo: 'Não autenticado — token ausente' };
    }
    console.log('[Auth] token válido');
    return this.passarAdiante(req);
  }
}

// ── Middleware 2: autorização ─────────────────────────────────
class AutorizacaoMiddleware extends MiddlewareBase {
  constructor(private readonly perfilExigido: 'admin' | 'usuario') {
    super();
  }

  processar(req: Requisicao): Resposta {
    if (req.perfil !== this.perfilExigido && req.perfil !== 'admin') {
      return { status: 403, corpo: `Acesso negado — exige perfil "${this.perfilExigido}"` };
    }
    console.log(`[Authz] perfil "${req.perfil}" autorizado`);
    return this.passarAdiante(req);
  }
}

// ── Middleware 3: throttle por IP ─────────────────────────────
class ThrottleMiddleware extends MiddlewareBase {
  private readonly contadores = new Map<string, number>();
  private readonly limite: number;

  constructor(requisicoesPorMinuto: number) {
    super();
    this.limite = requisicoesPorMinuto;
  }

  processar(req: Requisicao): Resposta {
    const count = (this.contadores.get(req.ip) ?? 0) + 1;
    this.contadores.set(req.ip, count);
    if (count > this.limite) {
      return { status: 429, corpo: `Limite excedido para IP ${req.ip}` };
    }
    console.log(`[Throttle] ${req.ip}: ${count}/${this.limite}`);
    return this.passarAdiante(req);
  }
}

// ── Handler final: rota ───────────────────────────────────────
class RotaHandler extends MiddlewareBase {
  processar(req: Requisicao): Resposta {
    return { status: 200, corpo: `GET ${req.path} — OK` };
  }
}

// ── Montagem da corrente ──────────────────────────────────────
const auth     = new AutenticacaoMiddleware();
const authz    = new AutorizacaoMiddleware('admin');
const throttle = new ThrottleMiddleware(3);
const rota     = new RotaHandler();

auth.setNext(authz).setNext(throttle).setNext(rota);

// ── Uso ──────────────────────────────────────────────────────
const reqs: Requisicao[] = [
  { path: '/admin', ip: '10.0.0.1' },                          // sem token
  { path: '/admin', token: 'abc', perfil: 'usuario', ip: '10.0.0.2' }, // sem perfil
  { path: '/admin', token: 'xyz', perfil: 'admin', ip: '10.0.0.3' },   // ok
  { path: '/admin', token: 'xyz', perfil: 'admin', ip: '10.0.0.3' },   // ok
  { path: '/admin', token: 'xyz', perfil: 'admin', ip: '10.0.0.3' },   // ok
  { path: '/admin', token: 'xyz', perfil: 'admin', ip: '10.0.0.3' },   // throttle
];

for (const req of reqs) {
  const res = auth.processar(req);
  console.log(`${res.status} — ${res.corpo}\n`);
}
// 401 — Não autenticado — token ausente
// 403 — Acesso negado — exige perfil "admin"
// [Auth] → [Authz] → [Throttle] → 200 — GET /admin — OK
// ... (repetido)
// 429 — Limite excedido para IP 10.0.0.3

Quando usar

  • Quando mais de um objeto pode processar uma requisição e o processador não é conhecido a priori: o remetente não deve embutir lógica de roteamento. A corrente decide dinamicamente quem age, baseado no estado da requisição.
  • Quando você quer enviar uma requisição para um de vários objetos sem especificar o receptor explicitamente: pipelines de middleware, filtros de eventos, fluxos de validação em estágios.
  • Quando o conjunto de handlers deve ser configurável em runtime: a corrente é montada pelo cliente; handlers podem ser adicionados, removidos ou reordenados sem modificar o remetente nem os handlers existentes.
  • Para implementar pipelines de validação ou pré-processamento onde cada etapa é autônoma: cada handler conhece apenas a sua regra e o contrato de repasse — não conhece os outros.

Quando evitar

  • Quando a garantia de processamento é obrigatória: o CoR não garante que a requisição será processada — ela pode cair no vazio se nenhum handler a aceitar. Se o processamento é obrigatório, use um handler final de fallback ou escolha outro padrão.
  • Quando a corrente é muito longa ou aninhada: correntes longas são difíceis de depurar — rastrear por qual handler a requisição passou exige logging explícito em cada elo. O stack trace pode ser profundo e a origem do problema, obscura.
  • Quando há exatamente um handler possível e ele é conhecido estaticamente: nesse caso, uma chamada direta ou Strategy é mais simples e mais legível.

Prós e contras

Prós

  • Desacopla o remetente dos receptores — o remetente conhece apenas o primeiro handler da corrente.
  • Respeita o Princípio da Responsabilidade Única: cada handler tem uma regra de tratamento.
  • Respeita o Princípio Aberto/Fechado: novos handlers podem ser inseridos sem modificar os existentes ou o remetente.
  • A corrente é configurável em runtime — handlers podem ser reordenados, adicionados ou removidos dinamicamente.

Contras

  • Não garante processamento — a requisição pode cair no vazio se nenhum handler agir (requer handler de fallback intencional).
  • Dificulta a depuração: o fluxo de execução não é óbvio apenas lendo o código que monta a corrente.
  • Correntes longas podem ter impacto de performance — cada handler acrescenta uma chamada de método.
  • A responsabilidade pode se tornar difusa: quando muitos handlers têm regras sobrepostas, é difícil saber qual processará.

Armadilhas comuns

1. Requisição que cai no vazio (sem handler final)

O erro mais comum: montar a corrente sem um handler de fallback no final. Se nenhum handler da corrente aceitar a requisição, ela retorna null silenciosamente — e o código que chamou pode não estar preparado para isso, causando NullPointerException ou comportamento indefinido.

Regra prática: sempre adicione um handler final explícito que trate o caso "nenhum handler anterior quis processar". Pode ser um logger de warning, um lançador de exceção ou uma resposta de erro padrão. Nunca confie em null implícito como indicador de "não tratado".

2. Corrente que não encerra (loop)

Se um handler chamar super.handle() sem verificar a condição — ou se um handler for acidentalmente adicionado ao final de sua própria corrente — o CoR pode entrar em loop infinito ou em recursão profunda. Garanta que cada ConcreteHandler tem uma condição clara para encerrar a corrente e que nenhum handler referencia a si mesmo como próximo.

3. Responsabilidade difusa — handlers que "ouvem" em vez de "filtrar"

Um antipadrão frequente: handlers que sempre chamam super.handle() independentemente do resultado — processam E repassam. Isso transforma o CoR num Observer disfarçado, onde todos os handlers executam sempre. O Princípio da Responsabilidade Única do CoR é: ou você trata e encerra, ou não trata e passa. Se o domínio exige que todos tratem, use Observer ou uma lista de listeners.

4. Chain of Responsibility vs Strategy — distinção de intenção

A confusão surge porque ambos encapsulam comportamento em objetos com interface comum. A diferença é estrutural e de intenção:

  • Strategy: exatamente um algoritmo é escolhido pelo contexto e aplicado. Não há corrente, não há repasse. O contexto decide qual Strategy usar — e só aquela executa.
  • Chain of Responsibility: zero ou mais handlers processam a requisição sequencialmente, cada um podendo encerrar ou continuar. Nenhum conhece os outros. O conjunto que processará é determinado em runtime pelo estado da requisição.

5. Distinção com Decorator

Estruturalmente similares: ambos encadeiam objetos com a mesma interface. A diferença é que o Decorator sempre delega ao próximo (acrescentando comportamento antes ou depois, nunca interrompendo) e tem como intenção adicionar responsabilidades ao objeto decorado. O CoR pode interromper a corrente sem delegar — quando processa a requisição, os handlers seguintes não são chamados. Se o objeto sempre passa adiante E adiciona comportamento, é Decorator. Se pode encerrar a corrente sem passar, é CoR.

Padrões relacionados

O Chain of Responsibility é frequentemente combinado ou comparado com padrões que também lidam com despacho de ações e composição de comportamento:

O Command e o CoR frequentemente aparecem juntos: o Command encapsula a requisição como objeto, e o CoR decide qual handler a processará — o Command carrega os dados da ação; o CoR roteia quem age sobre eles. O Decorator é o padrão de maior semelhança estrutural: ambos encadeiam objetos com a mesma interface. A diferença central é que o Decorator sempre delega ao próximo e adiciona comportamento sem interromper a cadeia, enquanto o CoR pode encerrar a corrente ao processar a requisição — os dois têm intenções opostas quanto ao fluxo de controle. O Visitor pode ser usado para processar os nós de uma corrente CoR sem modificar as classes de handler, separando a lógica de traversal da lógica de tratamento.