Design Patterns

Template Method

Define o esqueleto de um algoritmo na superclasse, delegando etapas às subclasses

Conceito e intenção

Template Method define a estrutura (o “esqueleto”) de um algoritmo em uma classe base, deixando as subclasses implementarem ou sobrescreverem etapas específicas sem alterar a estrutura geral. O algoritmo é fixo; as variações estão nas etapas.

O problema motivador é código duplicado entre variações de um mesmo processo. Imagine três importadores de dados: CSV, XML e JSON. Todos seguem o mesmo fluxo — ler fonte, validar, transformar, persistir, notificar. Sem Template Method, cada importador implementa o fluxo completo, duplicando a estrutura e divergindo gradualmente. Com Template Method, a estrutura vive na classe base e cada subclasse implementa apenas as etapas que variam.

A diferença fundamental em relação ao Strategy: Template Method usa herança — a variação está nas subclasses. Strategy usa composição — a variação é injetada. Para algoritmos com muitas etapas interdependentes que compartilham contexto, Template Method é mais natural. Para algoritmos que precisam ser trocados em runtime, Strategy é melhor.


Estrutura

┌─────────────────────────────────────────┐
│       AbstractClass                     │
│  + templateMethod() [final]             │
│    1. primitiveOp1() ◀─ abstract        │
│    2. primitiveOp2() ◀─ abstract        │
│    3. hook()         ◀─ opcional/vazio  │
└─────────────────────────────────────────┘

        ┌───────────┴───────────┐
 ┌──────┴───────┐       ┌───────┴──────┐
 │ConcreteClassA│       │ConcreteClassB│
 │+primitiveOp1 │       │+primitiveOp1 │
 │+primitiveOp2 │       │+primitiveOp2 │
 └──────────────┘       └──────────────┘

Participantes:

  • AbstractClass — define o template method (final) e declara as etapas primitivas
  • ConcreteClass — implementa as etapas obrigatórias; pode sobrescrever hooks

Implementação Java — pipeline de relatórios

// Classe base — define a estrutura do pipeline de geração de relatório
public abstract class ReportGenerator {

    // Template Method — a estrutura é fixada (final impede sobrescrita)
    public final Report generate(ReportRequest request) {
        // 1. Validar parâmetros
        validateRequest(request);

        // 2. Coletar dados
        List<DataRow> rawData = collectData(request);

        // 3. Filtrar (hook com implementação padrão)
        List<DataRow> filtered = applyFilters(rawData, request);

        // 4. Processar/agregar — subclasse DEVE implementar
        ReportData processedData = processData(filtered, request);

        // 5. Formatar — subclasse DEVE implementar
        byte[] content = format(processedData, request);

        // 6. Gerar metadados
        ReportMetadata metadata = buildMetadata(request, processedData.getRowCount());

        // 7. Hook pós-geração — subclasse PODE sobrescrever
        afterGenerate(request, metadata);

        return new Report(metadata, content);
    }

    // Etapas OBRIGATÓRIAS — subclasse DEVE implementar
    protected abstract List<DataRow> collectData(ReportRequest request);
    protected abstract ReportData processData(List<DataRow> data, ReportRequest request);
    protected abstract byte[] format(ReportData data, ReportRequest request);

    // Etapa com implementação padrão — subclasse PODE sobrescrever
    protected List<DataRow> applyFilters(List<DataRow> data, ReportRequest request) {
        // Filtro padrão: remove linhas nulas
        return data.stream()
            .filter(row -> row != null && !row.isEmpty())
            .collect(Collectors.toList());
    }

    // Hook vazio — subclasse decide se usa
    protected void afterGenerate(ReportRequest request, ReportMetadata metadata) {
        // implementação padrão vazia — hook opcional
    }

    // Etapa comum usada por todas as subclasses
    private void validateRequest(ReportRequest request) {
        Objects.requireNonNull(request.getStartDate(), "Data de início obrigatória");
        Objects.requireNonNull(request.getEndDate(), "Data de fim obrigatória");
        if (request.getStartDate().isAfter(request.getEndDate())) {
            throw new IllegalArgumentException("Data de início deve ser anterior ao fim");
        }
    }

    private ReportMetadata buildMetadata(ReportRequest request, int rowCount) {
        return ReportMetadata.builder()
            .reportType(getClass().getSimpleName())
            .generatedAt(Instant.now())
            .period(request.getStartDate(), request.getEndDate())
            .rowCount(rowCount)
            .build();
    }
}

// Relatório de vendas por período (coleta do banco, formata em CSV)
public class SalesReportGenerator extends ReportGenerator {

    private final SalesRepository salesRepository;
    private final CsvFormatter csvFormatter;
    private final ReportNotifier notifier;

    public SalesReportGenerator(
            SalesRepository salesRepository,
            CsvFormatter csvFormatter,
            ReportNotifier notifier) {
        this.salesRepository = salesRepository;
        this.csvFormatter    = csvFormatter;
        this.notifier        = notifier;
    }

    @Override
    protected List<DataRow> collectData(ReportRequest request) {
        return salesRepository.findByPeriod(request.getStartDate(), request.getEndDate())
            .stream()
            .map(sale -> DataRow.of(
                sale.getId(), sale.getProduct(), sale.getQuantity(), sale.getTotal()
            ))
            .collect(Collectors.toList());
    }

    @Override
    protected List<DataRow> applyFilters(List<DataRow> data, ReportRequest request) {
        List<DataRow> base = super.applyFilters(data, request); // chama implementação padrão
        // Filtro adicional: somente vendas confirmadas
        return base.stream()
            .filter(row -> "CONFIRMED".equals(row.get("status")))
            .collect(Collectors.toList());
    }

    @Override
    protected ReportData processData(List<DataRow> data, ReportRequest request) {
        // Agrupa por produto e soma totais
        Map<String, BigDecimal> byProduct = data.stream()
            .collect(Collectors.groupingBy(
                row -> row.get("product"),
                Collectors.reducing(BigDecimal.ZERO,
                    row -> new BigDecimal(row.get("total")),
                    BigDecimal::add)
            ));
        return ReportData.fromMap(byProduct);
    }

    @Override
    protected byte[] format(ReportData data, ReportRequest request) {
        return csvFormatter.format(data.toRows());
    }

    @Override
    protected void afterGenerate(ReportRequest request, ReportMetadata metadata) {
        // Notifica o solicitante por e-mail com o relatório
        notifier.sendReportReady(request.getRequestedBy(), metadata);
    }
}

// Relatório financeiro (coleta de API externa, formata em PDF)
public class FinancialReportGenerator extends ReportGenerator {

    private final FinancialApiClient apiClient;
    private final PdfFormatter pdfFormatter;

    public FinancialReportGenerator(FinancialApiClient apiClient, PdfFormatter pdfFormatter) {
        this.apiClient    = apiClient;
        this.pdfFormatter = pdfFormatter;
    }

    @Override
    protected List<DataRow> collectData(ReportRequest request) {
        // Busca de API externa em vez de banco local
        return apiClient.fetchTransactions(request.getStartDate(), request.getEndDate())
            .stream()
            .map(tx -> DataRow.of(tx.getId(), tx.getDate(), tx.getAmount(), tx.getCategory()))
            .collect(Collectors.toList());
    }

    @Override
    protected ReportData processData(List<DataRow> data, ReportRequest request) {
        // Agrupa por categoria
        Map<String, BigDecimal> byCategory = data.stream()
            .collect(Collectors.groupingBy(
                row -> row.get("category"),
                Collectors.reducing(BigDecimal.ZERO,
                    row -> new BigDecimal(row.get("amount")),
                    BigDecimal::add)
            ));
        return ReportData.fromMap(byCategory);
    }

    @Override
    protected byte[] format(ReportData data, ReportRequest request) {
        return pdfFormatter.format(data.toRows(), "Relatório Financeiro");
    }
    // afterGenerate não sobrescrito — não notifica
}

Exemplo: JUnit TestCase (Template Method clássico)

// JUnit 3 era Template Method puro
public class OrderServiceTest extends TestCase {

    private OrderService orderService;
    private MockPaymentGateway paymentGateway;

    // setUp() = hook chamado antes de cada teste
    @Override
    protected void setUp() throws Exception {
        paymentGateway = new MockPaymentGateway();
        orderService   = new OrderService(paymentGateway);
    }

    // tearDown() = hook chamado após cada teste
    @Override
    protected void tearDown() throws Exception {
        paymentGateway.reset();
    }

    public void testPlaceOrderSuccess() {
        // lógica do teste
    }
}
// O TestCase.runBare() era o template method: setUp() → runTest() → tearDown()

Implementação TypeScript

// Classe base com Template Method
abstract class ReportGenerator {

  // Template Method — estrutura fixa
  async generate(request: ReportRequest): Promise<Report> {
    this.validateRequest(request);
    const rawData      = await this.collectData(request);
    const filtered     = this.applyFilters(rawData, request);
    const processedData = this.processData(filtered, request);
    const content      = this.format(processedData);
    await this.afterGenerate(request, processedData);
    return { content, generatedAt: new Date(), rowCount: processedData.length };
  }

  // Métodos abstratos — subclasse DEVE implementar
  protected abstract collectData(request: ReportRequest): Promise<DataRow[]>;
  protected abstract processData(data: DataRow[], request: ReportRequest): ProcessedData[];
  protected abstract format(data: ProcessedData[]): Buffer;

  // Hook com implementação padrão — subclasse PODE sobrescrever
  protected applyFilters(data: DataRow[], _request: ReportRequest): DataRow[] {
    return data.filter(row => row && Object.keys(row).length > 0);
  }

  // Hook vazio opcional
  protected async afterGenerate(_request: ReportRequest, _data: ProcessedData[]): Promise<void> {}

  private validateRequest(request: ReportRequest): void {
    if (!request.startDate || !request.endDate) throw new Error('Período obrigatório');
    if (request.startDate > request.endDate) throw new Error('Período inválido');
  }
}

// Subclasse concreta
class SalesReportGenerator extends ReportGenerator {
  constructor(private readonly salesRepo: SalesRepository) {
    super();
  }

  protected async collectData(request: ReportRequest): Promise<DataRow[]> {
    const sales = await this.salesRepo.findByPeriod(request.startDate, request.endDate);
    return sales.map(s => ({ id: s.id, product: s.product, total: s.total }));
  }

  protected processData(data: DataRow[]): ProcessedData[] {
    // Agrupa por produto
    const grouped = data.reduce((acc, row) => {
      acc[row.product] = (acc[row.product] ?? 0) + row.total;
      return acc;
    }, {} as Record<string, number>);
    return Object.entries(grouped).map(([product, total]) => ({ product, total }));
  }

  protected format(data: ProcessedData[]): Buffer {
    const csv = ['produto,total', ...data.map(d => `${d.product},${d.total}`)].join('\n');
    return Buffer.from(csv, 'utf-8');
  }
}

No mundo real

HttpServlet do Jakarta EE — service() é o Template Method. Ele identifica o método HTTP e chama doGet(), doPost(), doPut() etc. Você sobrescreve apenas os métodos que precisa:

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // só o GET — template method já roteou
    }
}

Spring JdbcTemplateexecute(StatementCallback) é um template que abre conexão, executa o callback (etapa variável), e garante fechamento mesmo em exceção:

jdbcTemplate.query("SELECT * FROM orders WHERE id = ?",
    (rs, rowNum) -> new Order(rs.getString("id"), rs.getBigDecimal("total")),
    orderId);
// A query e o mapeamento variam; conexão/exception handling é fixo

Spring AbstractController, SimpleFormController (MVC legado) usavam Template Method para o ciclo de vida de requisições HTTP.

AbstractList, AbstractMap, AbstractSet no Java Collections — definem a maioria dos métodos em termos de get(), size(), iterator() que as subclasses implementam. Template Method para coleções.

JUnit @BeforeEach / @AfterEach — o framework (TestEngine) chama esses hooks em volta de cada @Test. É Template Method em nível de framework.

Spring Batch AbstractItemCountingItemStreamItemReader — template para leitores de batch: doOpen(), doRead(), doClose() são os primitives; o checkpointing e a contagem são fixos.


Quando usar

  • Processos com estrutura fixa mas etapas variáveis — ETL, importações, pipelines de processamento, relatórios
  • Múltiplas classes compartilham o mesmo esqueleto de algoritmo com pequenas variações
  • Você quer evitar duplicação de código de orquestração entre variações do mesmo processo
  • Quando hooks opcionais permitem customização sem forçar sobrescrita completa

Quando NÃO usar

  • Quando a variação ocorre em runtime e precisa ser trocada sem criar subclasses — use Strategy
  • Quando o algoritmo tem poucas etapas simples — a herança adiciona complexidade desnecessária
  • Quando as subclasses precisam chamar super.method() em pontos específicos (fragile base class problem) — considere Strategy com composição
  • Quando o uso de herança cria acoplamento rígido difícil de testar — injete as etapas como dependências

Combinações com outros padrões

Template Method + Strategy: Quando a mesma etapa do template precisa variar sem criar subclasses, extraia a etapa para um Strategy injetado:

public abstract class DataImporter {
    private final ValidationStrategy validator; // Strategy injetado

    protected abstract List<RawRecord> read(InputStream source);

    public final void run(InputStream source) {
        List<RawRecord> raw      = read(source);       // Template Method
        List<RawRecord> valid    = validator.validate(raw); // Strategy
        persist(valid);
    }
}

Template Method + Factory Method: Os “primitives” do Template Method frequentemente são factory methods — a subclasse sobrescreve createValidator(), createFormatter() para customizar o processo.

Template Method + Hook Pattern: Hooks (métodos com implementação padrão vazia) dão às subclasses pontos de extensão opcionais. Seguindo o Hollywood Principle: “Não nos chame — nós chamaremos você.”