Padrão Comportamental (GoF)

Template Method

Define o esqueleto de um algoritmo num método da classe-base, delegando passos específicos às subclasses via métodos abstratos e hooks — sem alterar a estrutura geral do algoritmo.

Intenção

Definir o esqueleto de um algoritmo na classe-base e deixar que as subclasses preencham os passos variáveis — sem alterar a estrutura geral do algoritmo. A classe-base chama os métodos que as subclasses implementam (não o contrário): é a inversão de controle, também chamada de princípio de Hollywood — "não nos chame, nós chamamos você".

Catalogado pelo GoF (1994) como padrão comportamental, o Template Method é a base de muitos frameworks: o framework define o fluxo geral e chama métodos que o desenvolvedor implementa na subclasse (hooks de ciclo de vida em React, Angular, Laravel, etc.). O padrão é também a estrutura-mãe do Factory Method, que é uma especialização: um Template Method cujo único passo variável é a criação de um objeto.

Problema

Considere um pipeline de geração de relatórios que sempre segue as mesmas etapas: coletar dados, processar, formatar e exportar. A lógica de coleta e processamento é idêntica para todos os relatórios, mas a formatação e exportação variam (CSV, PDF, HTML).

Sem o Template Method, duas abordagens comuns geram problemas:

  1. Duplicação: copiar o pipeline inteiro para cada subclasse. Mudanças no fluxo geral exigem atualizar todas as cópias — violação do princípio DRY.
  2. Condicional no algoritmo: uma única classe com if (formato === "csv") embutido no método de exportação — os mesmos problemas de crescimento e testabilidade do Strategy mal aplicado.

O Template Method resolve: a classe-base define o fluxo fixo e chama métodos abstratos/hooks nas posições variáveis. Cada subclasse implementa apenas os passos que diferem.

Solução

O Template Method organiza o código em dois participantes:

  1. AbstractClass (classe-base): define o template method — o método que contém o algoritmo completo em sequência. Chama métodos abstratos (que subclasses devem implementar) e hooks (que subclasses podem sobrescrever, mas têm implementação padrão). O template method geralmente é final (ou equivalente) para impedir que subclasses alterem a estrutura do algoritmo.
  2. ConcreteClass (subclasse): implementa apenas os passos variáveis declarados como abstratos na classe-base. Pode sobrescrever hooks para adicionar comportamento opcional sem alterar o fluxo geral.

Template Method vs Strategy

Ambos resolvem a variação de partes de um processo, mas de formas opostas:

  • Template Method usa herança: a variação é resolvida em tempo de compilação. A subclasse é criada uma vez e sempre executará os mesmos passos concretos. O algoritmo inteiro está na classe-base; apenas partes dele variam.
  • Strategy usa composição: a variação é resolvida em tempo de execução. O algoritmo inteiro pode ser trocado por injeção. Prefira Strategy quando o algoritmo inteiro varia; prefira Template Method quando apenas partes variam e a estrutura geral é fixa.

Factory Method como especialização

O Factory Method é um Template Method com um único passo variável: a criação do objeto. A classe-base define o template method de "criação" e delega para o método abstrato criarProduto() que cada subclasse implementa. Isso torna o Factory Method conceitualmente um caso especial do Template Method.

Estrutura

      GeradorRelatorio (AbstractClass)
  ┌───────────────────────────────────────────┐
  │ + gerar(): void  [template method]        │
  │     1. dados = coletarDados()             │
  │     2. proc  = processar(dados)           │
  │     3. aposProcessar(proc) [hook opcl.]   │
  │     4. out   = formatar(proc)             │
  │     5. exportar(out)                      │
  │                                           │
  │ # coletarDados(): Dados    [concreto]     │
  │ # processar(d): Proc       [concreto]     │
  │ # aposProcessar(p): void   [hook — vazio] │
  │ # formatar(p): string      [abstrato]     │
  │ # exportar(s): void        [abstrato]     │
  └───────────────────────────────────────────┘
              ▲
   ┌──────────┴──────────┐
   │                     │
RelatorioCSV         RelatorioPDF
(ConcreteClass)      (ConcreteClass)
formatar → CSV       formatar → PDF
exportar → arquivo   exportar → impressora

Exemplos de código

Exemplo 1 — Pipeline de geração de relatórios

A classe-base define o fluxo completo. Subclasses implementam apenas formatação e exportação. Um hook opcional permite customização sem tornar o passo obrigatório.

// Tipos simples para o exemplo.
type DadosBrutos = { titulo: string; valores: number[] };
type DadosProcessados = { titulo: string; soma: number; media: number; count: number };

// ── AbstractClass ─────────────────────────────────────────────
abstract class GeradorRelatorio {

  // Template Method — define o algoritmo completo. Subclasses
  // nao devem sobrescrever este metodo (use readonly na pratica).
  gerar(titulo: string, valores: number[]): void {
    const brutos   = this.coletarDados(titulo, valores);   // fixo
    const proc     = this.processar(brutos);               // fixo
    this.aposProcessar(proc);                              // hook (opcional)
    const saida    = this.formatar(proc);                  // abstrato
    this.exportar(saida);                                  // abstrato
  }

  // Passos concretos — implementados na classe-base (logica comum).
  protected coletarDados(titulo: string, valores: number[]): DadosBrutos {
    return { titulo, valores };
  }

  protected processar(d: DadosBrutos): DadosProcessados {
    const soma = d.valores.reduce((acc, v) => acc + v, 0);
    return {
      titulo: d.titulo,
      soma,
      media: d.valores.length ? soma / d.valores.length : 0,
      count: d.valores.length,
    };
  }

  // Hook — implementacao padrao vazia; subclasses podem sobrescrever.
  protected aposProcessar(_proc: DadosProcessados): void { /* vazio */ }

  // Passos abstratos — obrigatorios nas subclasses.
  protected abstract formatar(proc: DadosProcessados): string;
  protected abstract exportar(saida: string): void;
}

// ── ConcreteClass: CSV ────────────────────────────────────────
class RelatorioCSV extends GeradorRelatorio {
  protected formatar(proc: DadosProcessados): string {
    return [
      "titulo,soma,media,count",
      `${proc.titulo},${proc.soma},${proc.media.toFixed(2)},${proc.count}`,
    ].join("\n");
  }

  protected exportar(saida: string): void {
    console.log("[CSV] Exportando:\n" + saida);
  }
}

// ── ConcreteClass: resumo de texto ───────────────────────────
class RelatorioTexto extends GeradorRelatorio {
  // Sobrescreve o hook para logar antes de formatar.
  protected override aposProcessar(proc: DadosProcessados): void {
    console.log(`[Hook] Processamento concluido para: "${proc.titulo}"`);
  }

  protected formatar(proc: DadosProcessados): string {
    return (
      `=== ${proc.titulo} ===\n` +
      `Total: ${proc.soma}  |  Media: ${proc.media.toFixed(2)}  |  Itens: ${proc.count}`
    );
  }

  protected exportar(saida: string): void {
    console.log("[TEXTO] Imprimindo:\n" + saida);
  }
}

// ── Uso ──────────────────────────────────────────────────────
const csv   = new RelatorioCSV();
const texto = new RelatorioTexto();

const valores = [10, 20, 30, 40];

csv.gerar("Vendas Junho", valores);
// [CSV] Exportando:
// titulo,soma,media,count
// Vendas Junho,100,25.00,4

texto.gerar("Vendas Junho", valores);
// [Hook] Processamento concluido para: "Vendas Junho"
// [TEXTO] Imprimindo:
// === Vendas Junho ===
// Total: 100  |  Media: 25.00  |  Itens: 4

Exemplo 2 — Parser de dados com etapas fixas e variáveis

Um segundo exemplo realista: pipeline de parsing onde abertura, fechamento e validação são fixos, mas a lógica de leitura linha-a-linha varia por formato (CSV vs formato de log). Ilustra também um hook que adiciona comportamento opcional entre etapas.

// ── AbstractClass ─────────────────────────────────────────────
abstract class Parser {
  // Template Method — define o fluxo de parsing.
  parse(conteudo: string): string[] {
    const linhas = this.dividirLinhas(conteudo);           // fixo
    const filtradas = this.filtrarLinhas(linhas);          // hook (padrao: passa tudo)
    const resultado: string[] = [];
    for (const linha of filtradas) {
      const item = this.parseLinha(linha);                 // abstrato
      if (item !== null) resultado.push(item);
    }
    this.aposParser(resultado);                            // hook (padrao: vazio)
    return resultado;
  }

  // Passo fixo: divide por linha.
  private dividirLinhas(conteudo: string): string[] {
    return conteudo.split("\n").map(l => l.trim()).filter(l => l.length > 0);
  }

  // Hook: filtragem antes do parse. Padrao: sem filtro.
  protected filtrarLinhas(linhas: string[]): string[] {
    return linhas;
  }

  // Abstrato: cada parser implementa o parse de uma linha.
  protected abstract parseLinha(linha: string): string | null;

  // Hook: acao apos o parse completo. Padrao: vazio.
  protected aposParser(_resultado: string[]): void { /* vazio */ }
}

// ── Concrete: parser de CSV (primeira coluna) ─────────────────
class ParserCSV extends Parser {
  constructor(private readonly colunaIndex: number = 0) { super(); }

  // Ignora linhas de cabecalho (comecam com "#").
  protected override filtrarLinhas(linhas: string[]): string[] {
    return linhas.filter(l => !l.startsWith("#"));
  }

  protected parseLinha(linha: string): string | null {
    const partes = linha.split(",");
    return partes[this.colunaIndex]?.trim() ?? null;
  }
}

// ── Concrete: parser de log (extrai nivel de severity) ────────
class ParserLog extends Parser {
  private erros = 0;

  protected parseLinha(linha: string): string | null {
    // Espera formato: "[NIVEL] mensagem"
    const match = linha.match(/^\[([A-Z]+)\]\s+(.+)$/);
    if (!match) return null;
    if (match[1] === "ERROR") this.erros++;
    return `${match[1]}: ${match[2]}`;
  }

  protected override aposParser(resultado: string[]): void {
    console.log(`[Log] ${resultado.length} linhas parseadas, ${this.erros} erros.`);
  }
}

// ── Uso ──────────────────────────────────────────────────────
const csv = new ParserCSV(0);
const csvDados = "# cabecalho ignorado\nAlice,30,SP\nBob,25,RJ\nCarla,28,MG";
console.log(csv.parse(csvDados));
// ["Alice", "Bob", "Carla"]

const log = new ParserLog();
const logDados = "[INFO] Sistema iniciado\n[ERROR] Falha na conexao\n[INFO] Tentando reconectar\n[ERROR] Timeout";
console.log(log.parse(logDados));
// [Log] 4 linhas parseadas, 2 erros.
// ["INFO: Sistema iniciado", "ERROR: Falha na conexao", "INFO: Tentando reconectar", "ERROR: Timeout"]

Quando usar

  • Quando múltiplas classes compartilham o mesmo algoritmo geral mas diferem em passos específicos — pipelines de processamento, importadores, parsers, geradores de relatório, fluxos de autenticação.
  • Para eliminar duplicação de código: a lógica comum fica na classe-base uma única vez; as subclasses implementam apenas o que é diferente. Mudanças no fluxo geral afetam um único lugar.
  • Para criar frameworks e bibliotecas extensíveis: o framework define o fluxo (template method) e expõe hooks para que o usuário customize comportamentos sem precisar entender o fluxo interno completo.
  • Quando a variação é em compile-time (subclasse escolhida na configuração ou na inicialização, não em runtime): Template Method é mais simples que Strategy nesse cenário.

Quando evitar

  • Quando o algoritmo inteiro varia: se cada variação não compartilha quase nada com as outras, Template Method força subclasses que implementam quase tudo — use Strategy (composição) em vez de herança.
  • Quando a variação precisa acontecer em runtime: herança é estática. Se você precisa trocar o comportamento enquanto o sistema roda, Strategy é mais adequado.
  • Quando a hierarquia de herança já é profunda: adicionar mais um nível de herança aumenta a complexidade e o acoplamento estrutural. Considere composição.

Prós e contras

Prós

  • Elimina duplicação: a lógica comum vive na classe-base; subclasses implementam apenas as variações.
  • O fluxo do algoritmo é controlado e centralizado — subclasses não podem alterar a ordem dos passos se o template method for final.
  • Hooks permitem extensão opcional sem tornar o passo obrigatório.
  • Facilita a criação de frameworks extensíveis com pontos de customização bem definidos.

Contras

  • Herança é um acoplamento forte: a subclasse está permanentemente ligada à estrutura da classe-base.
  • O Princípio de Substituição de Liskov pode ser violado se subclasses sobrescreverem passos de maneiras incompatíveis com o contrato geral.
  • Hooks demais tornam o template method difícil de entender — fica obscuro quais passos são realmente variáveis.
  • Para adicionar um novo passo ao algoritmo na classe-base, todas as subclasses existentes podem precisar ser atualizadas.

Armadilhas comuns

1. Princípio de Hollywood — "não nos chame, nós chamamos você"

O template method é chamado pelo cliente; ele chama os métodos das subclasses. O erro é a subclasse chamar diretamente o template method ou outros métodos da classe-base de formas não previstas — quebrando o contrato de inversão de controle. Use final (ou o equivalente na linguagem) no template method para garantir que a estrutura do algoritmo não seja sobrescrita.

2. Hooks demais

Atenção: cada hook é um ponto de extensão opcional que aumenta a complexidade do contrato da classe-base. Se houver muitos hooks, a subclasse precisa entender o fluxo completo para saber quais sobrescrever — perdendo a vantagem de abstração. Regra prática: exponha apenas os hooks que o cenário real de extensão exige. Hooks não usados são ruído.

3. Rigidez da herança

Template Method acopla a subclasse à estrutura da classe-base em tempo de compilação. Se o fluxo do algoritmo precisar mudar (ex.: adicionar um passo entre dois passos existentes), todas as subclasses podem ser afetadas. Para fluxos que evoluem muito, considere Strategy (composição de passos como objetos injetáveis) ou um pipeline baseado em Chain of Responsibility.

4. Subclasse que anula um passo sem respeitar o contrato

Se uma subclasse sobrescreve um passo abstrato e retorna um valor que viola o contrato esperado pela classe-base (ex.: retorna null quando o template method espera uma string), o algoritmo inteiro pode falhar de forma silenciosa ou com erros difíceis de rastrear. Documente claramente as invariantes de cada passo abstrato e valide-as no template method quando necessário.

Padrões relacionados

O Template Method interage com outros padrões de criação e comportamentais:

O Factory Method é uma especialização do Template Method: o template method tem um único passo variável que é a criação de um objeto — o método de fábrica abstrato. Toda a estrutura de herança do Factory Method é, conceitualmente, um Template Method aplicado à criação. O Strategy é o "primo de composição" do Template Method: resolve o mesmo problema de variação de comportamento mas com composição em vez de herança — preferível quando a variação precisa acontecer em runtime ou quando hierarquias de herança profundas são indesejáveis. O Observer pode ser combinado com Template Method: o template method define o fluxo de processamento e, em um dos passos (hook ou passo fixo), notifica Observers sobre o progresso ou resultado — desacoplando consumidores do resultado sem alterar o fluxo do algoritmo.