← Todos os padrões

Template Method

Behavioral · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Behavioral

O problema

Várias variantes de um processo compartilham a mesma forma geral — os mesmos passos, na mesma ordem — mas diferem em como um ou dois desses passos são de fato executados. Duplicar o processo inteiro pra cada variante faz com que as partes compartilhadas (ordenação, tratamento de erro, qualquer coisa que não deveria variar) se distanciem com o tempo, e uma correção de bug na lógica compartilhada precisa ser aplicada a cada cópia separadamente.

A solução

Colocar a sequência fixa de passos numa classe base como um método final, com cada passo delegado a um método abstrato (ou um "hook" opcionalmente sobrescrevível). Subclasses preenchem os passos; elas não conseguem reordenar, pular, ou duplicar a própria sequência, porque nunca a enxergam.

classDiagram
    class AbstractClass {
        +templateMethod() final
        #stepOne() abstract
        #stepTwo() abstract
        #hook()
    }
    class ConcreteClassA
    class ConcreteClassB
    AbstractClass <|-- ConcreteClassA
    AbstractClass <|-- ConcreteClassB

Exemplo clássico

classic/Game fixa a sequência initialize() → startPlay() → endPlay() → announceWinner() num play() final. Chess e Checkers implementam os três passos obrigatórios de forma diferente, e announceWinner() é um hook — um passo com uma implementação padrão vazia que uma subclasse pode sobrescrever mas não é obrigada a. Chess o sobrescreve; Checkers não, e isso é uma escolha completamente válida. GameTest verifica a ordem exata dos passos pros dois, e que o log do Checkers tem uma entrada a menos que o do Chess porque ele deixou o hook no padrão.

Exemplo aplicado: pipeline de migração de sistema legado

applied/LegacyMigrationPipeline fixa read → validate → transform → write num migrate() final. CobolFixedWidthMigrationPipeline faz parsing de registros posicionais de largura fixa; CsvLegacyMigrationPipeline faz parsing de registros separados por vírgula — dois formatos de exportação legados da mesma época, ambos migrando pro mesmo formato JSON moderno através da mesma forma de pipeline. Como validate() roda antes de transform()/write() dentro da sequência fixa, uma falha de validação nunca pode acidentalmente chegar ao MigrationSink — nenhuma subclasse consegue errar essa ordenação, porque nenhuma subclasse controla a ordenação. LegacyMigrationPipelineTest cobre os dois formatos migrando com sucesso e os dois rejeitando um registro malformado antes que qualquer coisa seja escrita.

Quando não usar

Cobertura de testes

100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :behavioral:templatemethod:jacocoTestReport

Relatório em behavioral/templatemethod/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/designpatterns/behavioral/templatemethod/classic/GameTest.java
package com.designpatterns.behavioral.templatemethod.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class GameTest {

    @Test
    void chessRunsEveryStepInOrderIncludingTheOverriddenHook() {
        Chess chess = new Chess();

        chess.play();

        assertThat(chess.log()).containsExactly(
                "Chess: setting up 32 pieces",
                "Chess: white moves first",
                "Chess: checkmate declared",
                "Chess: white wins by checkmate"
        );
    }

    @Test
    void checkersRunsTheRequiredStepsAndSkipsTheUnoverriddenHook() {
        Checkers checkers = new Checkers();

        checkers.play();

        assertThat(checkers.log()).containsExactly(
                "Checkers: setting up 24 pieces",
                "Checkers: dark pieces move first",
                "Checkers: no more legal moves for one side"
        );
    }
}
src/test/java/com/designpatterns/behavioral/templatemethod/applied/LegacyMigrationPipelineTest.java
package com.designpatterns.behavioral.templatemethod.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class LegacyMigrationPipelineTest {

    @Test
    void migratesAFixedWidthCobolRecordIntoTheModernFormat() {
        InMemoryMigrationSink sink = new InMemoryMigrationSink();
        LegacyMigrationPipeline pipeline = new CobolFixedWidthMigrationPipeline(sink);
        String rawRecord = String.format("%-20s%-10s", "JOAO DA SILVA", "15000");

        pipeline.migrate(rawRecord);

        assertThat(sink.records()).containsExactly("{\"name\":\"JOAO DA SILVA\",\"amountCents\":15000}");
    }

    @Test
    void migratesACsvRecordIntoTheSameModernFormat() {
        InMemoryMigrationSink sink = new InMemoryMigrationSink();
        LegacyMigrationPipeline pipeline = new CsvLegacyMigrationPipeline(sink);

        pipeline.migrate("Maria Souza, 8000");

        assertThat(sink.records()).containsExactly("{\"name\":\"Maria Souza\",\"amountCents\":8000}");
    }

    @Test
    void aValidationFailureStopsThePipelineBeforeAnythingReachesTheSink() {
        InMemoryMigrationSink sink = new InMemoryMigrationSink();
        LegacyMigrationPipeline pipeline = new CsvLegacyMigrationPipeline(sink);

        assertThatThrownBy(() -> pipeline.migrate("no-comma-here"))
                .isInstanceOf(IllegalStateException.class);
        assertThat(sink.records()).isEmpty();
    }

    @Test
    void theCobolPipelineRejectsARecordWithNoName() {
        InMemoryMigrationSink sink = new InMemoryMigrationSink();
        LegacyMigrationPipeline pipeline = new CobolFixedWidthMigrationPipeline(sink);
        String rawRecord = String.format("%-20s%-10s", "", "15000");

        assertThatThrownBy(() -> pipeline.migrate(rawRecord))
                .isInstanceOf(IllegalStateException.class);
        assertThat(sink.records()).isEmpty();
    }
}

Ver relatório completo de cobertura JaCoCo →