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 JdbcTemplate — execute(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 é fixoSpring 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ê.”