O Template Method (Método Template) é um padrão comportamental que define o esqueleto de um algoritmo em uma operação, delegando alguns passos para subclasses. Em Rust, este padrão se implementa de forma natural com traits que possuem métodos default: o método default define o algoritmo geral, e métodos sem implementação padrão servem como pontos de customização que cada tipo deve implementar.
A beleza desta abordagem em Rust é que o compilador garante que todos os pontos de customização sejam implementados, enquanto o algoritmo geral é herdado automaticamente. Diferente de herança em OOP (que cria acoplamento), traits em Rust oferecem composição limpa sem os problemas do “frágil base class”.
Problema
Imagine que você precisa processar diferentes tipos de arquivos (CSV, JSON, XML). O fluxo é sempre o mesmo: abrir o arquivo, ler o conteúdo, parsear os dados, validar, transformar e produzir resultado. Mas cada formato tem sua própria lógica de parsing e validação.
Sem o Template Method, cada processador repetiria toda a lógica do fluxo:
// ANTI-PADRÃO: código duplicado em cada processador
fn processar_csv(caminho: &str) -> Result<Vec<Registro>, Erro> {
let conteudo = ler_arquivo(caminho)?; // duplicado
let dados = parsear_csv(&conteudo)?; // específico
validar_dados(&dados)?; // duplicado
let resultado = transformar(dados); // duplicado
Ok(resultado)
}
fn processar_json(caminho: &str) -> Result<Vec<Registro>, Erro> {
let conteudo = ler_arquivo(caminho)?; // duplicado
let dados = parsear_json(&conteudo)?; // específico
validar_dados(&dados)?; // duplicado
let resultado = transformar(dados); // duplicado
Ok(resultado)
}
Solução em Rust
Template Method com Traits
use std::fmt;
/// Registro genérico — resultado do processamento
#[derive(Debug, Clone)]
struct Registro {
campos: Vec<(String, String)>,
}
impl Registro {
fn novo() -> Self {
Registro { campos: Vec::new() }
}
fn adicionar(&mut self, chave: &str, valor: &str) {
self.campos.push((chave.to_string(), valor.to_string()));
}
fn obter(&self, chave: &str) -> Option<&str> {
self.campos.iter()
.find(|(k, _)| k == chave)
.map(|(_, v)| v.as_str())
}
}
impl fmt::Display for Registro {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let campos: Vec<String> = self.campos.iter()
.map(|(k, v)| format!("{}: {}", k, v))
.collect();
write!(f, "{{{}}}", campos.join(", "))
}
}
/// Erros possíveis no processamento
#[derive(Debug)]
enum ErroProcessamento {
Leitura(String),
Parse(String),
Validacao(String),
Transformacao(String),
}
impl fmt::Display for ErroProcessamento {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroProcessamento::Leitura(m) => write!(f, "Erro de leitura: {}", m),
ErroProcessamento::Parse(m) => write!(f, "Erro de parse: {}", m),
ErroProcessamento::Validacao(m) => write!(f, "Erro de validação: {}", m),
ErroProcessamento::Transformacao(m) => write!(f, "Erro de transformação: {}", m),
}
}
}
/// Trait com Template Method — define o algoritmo e os pontos de customização
trait ProcessadorArquivo {
// ============================================================
// TEMPLATE METHOD — define o fluxo completo (não deve ser sobrescrito)
// ============================================================
/// Processa um arquivo do início ao fim
/// Este método default define o "template" — o esqueleto do algoritmo
fn processar(&self, conteudo: &str) -> Result<Vec<Registro>, ErroProcessamento> {
// Passo 1: Hooks de pré-processamento
let conteudo_preparado = self.pre_processar(conteudo);
// Passo 2: Parsing (OBRIGATÓRIO — cada formato implementa)
println!("[{}] Parseando dados...", self.nome_formato());
let registros = self.parsear(&conteudo_preparado)?;
println!("[{}] {} registros parseados", self.nome_formato(), registros.len());
// Passo 3: Validação (customizável, com default)
println!("[{}] Validando registros...", self.nome_formato());
let registros_validos = self.validar(registros)?;
println!("[{}] {} registros válidos", self.nome_formato(), registros_validos.len());
// Passo 4: Transformação (customizável, com default)
println!("[{}] Transformando registros...", self.nome_formato());
let resultado = self.transformar(registros_validos)?;
// Passo 5: Hook pós-processamento
self.pos_processar(&resultado);
Ok(resultado)
}
// ============================================================
// PONTOS DE CUSTOMIZAÇÃO — métodos abstratos (sem default)
// ============================================================
/// Nome do formato para logging
fn nome_formato(&self) -> &str;
/// Parse específico do formato — OBRIGATÓRIO
fn parsear(&self, conteudo: &str) -> Result<Vec<Registro>, ErroProcessamento>;
// ============================================================
// HOOKS — métodos com implementação default (opcionais)
// ============================================================
/// Pré-processamento do conteúdo bruto
/// Default: remove espaços em branco nas extremidades
fn pre_processar(&self, conteudo: &str) -> String {
conteudo.trim().to_string()
}
/// Validação dos registros parseados
/// Default: aceita todos (sem validação)
fn validar(&self, registros: Vec<Registro>) -> Result<Vec<Registro>, ErroProcessamento> {
Ok(registros)
}
/// Transformação dos registros validados
/// Default: retorna como está (sem transformação)
fn transformar(&self, registros: Vec<Registro>) -> Result<Vec<Registro>, ErroProcessamento> {
Ok(registros)
}
/// Hook pós-processamento
/// Default: não faz nada
fn pos_processar(&self, registros: &[Registro]) {
let _ = registros; // Silencia warning de variável não usada
}
}
// ============================================================
// Implementação: Processador CSV
// ============================================================
struct ProcessadorCsv {
separador: char,
tem_cabecalho: bool,
}
impl ProcessadorCsv {
fn novo() -> Self {
ProcessadorCsv {
separador: ',',
tem_cabecalho: true,
}
}
fn com_separador(mut self, sep: char) -> Self {
self.separador = sep;
self
}
}
impl ProcessadorArquivo for ProcessadorCsv {
fn nome_formato(&self) -> &str {
"CSV"
}
fn parsear(&self, conteudo: &str) -> Result<Vec<Registro>, ErroProcessamento> {
let mut linhas = conteudo.lines();
// Extrair cabeçalhos
let cabecalhos: Vec<String> = if self.tem_cabecalho {
match linhas.next() {
Some(linha) => linha.split(self.separador)
.map(|c| c.trim().to_string())
.collect(),
None => return Err(ErroProcessamento::Parse("Arquivo vazio".to_string())),
}
} else {
// Gerar nomes genéricos
(0..10).map(|i| format!("coluna_{}", i)).collect()
};
// Parsear linhas de dados
let mut registros = Vec::new();
for (num_linha, linha) in linhas.enumerate() {
if linha.trim().is_empty() {
continue;
}
let valores: Vec<&str> = linha.split(self.separador).collect();
if valores.len() != cabecalhos.len() {
return Err(ErroProcessamento::Parse(format!(
"Linha {}: esperado {} colunas, encontrado {}",
num_linha + 2, cabecalhos.len(), valores.len()
)));
}
let mut registro = Registro::novo();
for (cab, val) in cabecalhos.iter().zip(valores.iter()) {
registro.adicionar(cab, val.trim());
}
registros.push(registro);
}
Ok(registros)
}
/// Validação específica para CSV: verificar campos não vazios
fn validar(&self, registros: Vec<Registro>) -> Result<Vec<Registro>, ErroProcessamento> {
let validos: Vec<Registro> = registros.into_iter()
.filter(|r| r.campos.iter().all(|(_, v)| !v.is_empty()))
.collect();
Ok(validos)
}
}
// ============================================================
// Implementação: Processador de Pares Chave=Valor
// ============================================================
struct ProcessadorChaveValor {
separador_par: char,
separador_registro: char,
}
impl ProcessadorChaveValor {
fn novo() -> Self {
ProcessadorChaveValor {
separador_par: '=',
separador_registro: '\n',
}
}
}
impl ProcessadorArquivo for ProcessadorChaveValor {
fn nome_formato(&self) -> &str {
"ChaveValor"
}
fn parsear(&self, conteudo: &str) -> Result<Vec<Registro>, ErroProcessamento> {
let mut registros = Vec::new();
let mut registro_atual = Registro::novo();
let mut tem_dados = false;
for linha in conteudo.lines() {
let linha = linha.trim();
// Linha vazia separa registros
if linha.is_empty() {
if tem_dados {
registros.push(registro_atual);
registro_atual = Registro::novo();
tem_dados = false;
}
continue;
}
// Ignorar comentários
if linha.starts_with('#') {
continue;
}
// Parsear chave=valor
let partes: Vec<&str> = linha.splitn(2, self.separador_par).collect();
if partes.len() != 2 {
return Err(ErroProcessamento::Parse(
format!("Formato inválido: '{}'", linha),
));
}
registro_atual.adicionar(partes[0].trim(), partes[1].trim());
tem_dados = true;
}
// Último registro
if tem_dados {
registros.push(registro_atual);
}
Ok(registros)
}
/// Transformação: normalizar chaves para minúsculas
fn transformar(&self, registros: Vec<Registro>) -> Result<Vec<Registro>, ErroProcessamento> {
let transformados = registros.into_iter().map(|r| {
let mut novo = Registro::novo();
for (chave, valor) in &r.campos {
novo.adicionar(&chave.to_lowercase(), valor);
}
novo
}).collect();
Ok(transformados)
}
fn pos_processar(&self, registros: &[Registro]) {
println!("[ChaveValor] Pós-processamento: {} registros finalizados", registros.len());
}
}
// ============================================================
// Implementação: Processador de Linhas Fixas
// ============================================================
struct ProcessadorLinhaFixa {
definicao_campos: Vec<(String, usize, usize)>, // (nome, início, tamanho)
}
impl ProcessadorLinhaFixa {
fn novo(campos: Vec<(&str, usize, usize)>) -> Self {
ProcessadorLinhaFixa {
definicao_campos: campos.into_iter()
.map(|(n, i, t)| (n.to_string(), i, t))
.collect(),
}
}
}
impl ProcessadorArquivo for ProcessadorLinhaFixa {
fn nome_formato(&self) -> &str {
"LinhaFixa"
}
fn parsear(&self, conteudo: &str) -> Result<Vec<Registro>, ErroProcessamento> {
let mut registros = Vec::new();
for (num_linha, linha) in conteudo.lines().enumerate() {
if linha.trim().is_empty() {
continue;
}
let mut registro = Registro::novo();
for (nome, inicio, tamanho) in &self.definicao_campos {
let fim = inicio + tamanho;
if fim > linha.len() {
return Err(ErroProcessamento::Parse(format!(
"Linha {}: campo '{}' excede o tamanho ({}..{}, linha tem {})",
num_linha + 1, nome, inicio, fim, linha.len()
)));
}
let valor = linha[*inicio..fim].trim();
registro.adicionar(nome, valor);
}
registros.push(registro);
}
Ok(registros)
}
fn pre_processar(&self, conteudo: &str) -> String {
// Remover linhas de cabeçalho (comum em arquivos de linha fixa)
conteudo.lines()
.skip(1) // Pula a primeira linha (cabeçalho)
.collect::<Vec<&str>>()
.join("\n")
}
}
fn main() {
// ============================================================
// Processando CSV
// ============================================================
println!("{'='repeated 50}");
println!("Processando CSV");
println!("{'='repeated 50}");
let csv = "nome,email,idade\nAlice,alice@ex.com,30\nBob,bob@ex.com,25\nCarol,,28";
let processador_csv = ProcessadorCsv::novo();
match processador_csv.processar(csv) {
Ok(registros) => {
println!("\nResultado:");
for r in ®istros {
println!(" {}", r);
}
}
Err(e) => println!("Erro: {}", e),
}
// ============================================================
// Processando Chave=Valor
// ============================================================
println!("\n{'='repeated 50}");
println!("Processando Chave=Valor");
println!("{'='repeated 50}");
let kv = "# Configuração do servidor\nHost=localhost\nPorta=8080\nDebug=true\n\nHost=remoto.com\nPorta=443\nDebug=false";
let processador_kv = ProcessadorChaveValor::novo();
match processador_kv.processar(kv) {
Ok(registros) => {
println!("\nResultado:");
for r in ®istros {
println!(" {}", r);
}
}
Err(e) => println!("Erro: {}", e),
}
// ============================================================
// Processando Linha Fixa
// ============================================================
println!("\n{'='repeated 50}");
println!("Processando Linha Fixa");
println!("{'='repeated 50}");
let fixa = "NOME CIDADE IDADE\nAlice São Paulo 030\nBob Rio 025";
let processador_fixa = ProcessadorLinhaFixa::novo(vec![
("nome", 0, 10),
("cidade", 10, 10),
("idade", 20, 3),
]);
match processador_fixa.processar(fixa) {
Ok(registros) => {
println!("\nResultado:");
for r in ®istros {
println!(" {}", r);
}
}
Err(e) => println!("Erro: {}", e),
}
}
Diagrama
Template Method — Fluxo do Algoritmo:
trait ProcessadorArquivo {
fn processar() ← TEMPLATE METHOD (define o fluxo)
┌──────────────────────────────────────────┐
│ 1. pre_processar() ← hook (default) │
│ 2. parsear() ← ABSTRATO (obrig.) │
│ 3. validar() ← hook (default) │
│ 4. transformar() ← hook (default) │
│ 5. pos_processar() ← hook (default) │
└──────────────────────────────────────────┘
}
Implementações:
┌───────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ProcessadorCsv │ │ Processador │ │ Processador │
│ │ │ ChaveValor │ │ LinhaFixa │
│───────────────────│ │─────────────────│ │─────────────────│
│ parsear() ✓ │ │ parsear() ✓ │ │ parsear() ✓ │
│ validar() ✓ │ │ transformar()✓ │ │ pre_proc. ✓ │
│ (customizado) │ │ pos_proc() ✓ │ │ (customizado) │
│ │ │ (customizados) │ │ │
│ pre_processar() │ │ pre_processar() │ │ validar() │
│ transformar() │ │ validar() │ │ transformar() │
│ pos_processar() │ │ (herdados) │ │ pos_processar() │
│ (herdados) │ │ │ │ (herdados) │
└───────────────────┘ └─────────────────┘ └─────────────────┘
Pontos de customização:
┌──────────────┬────────────────┬───────────────────────────┐
│ Tipo │ Obrigatório? │ Propósito │
├──────────────┼────────────────┼───────────────────────────┤
│ parsear() │ SIM (sem │ Lógica específica do │
│ │ default) │ formato de arquivo │
│──────────────┼────────────────┼───────────────────────────│
│ pre_proc() │ NÃO (default) │ Preparação opcional │
│ validar() │ NÃO (default) │ Validação opcional │
│ transformar()│ NÃO (default) │ Transformação opcional │
│ pos_proc() │ NÃO (default) │ Finalização opcional │
└──────────────┴────────────────┴───────────────────────────┘
Exemplo do Mundo Real
Um framework de testes simplificado com setup/teardown template:
/// Resultado de um teste
#[derive(Debug)]
enum ResultadoTeste {
Passou,
Falhou(String),
Pulado(String),
}
impl fmt::Display for ResultadoTeste {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResultadoTeste::Passou => write!(f, "PASSOU"),
ResultadoTeste::Falhou(msg) => write!(f, "FALHOU: {}", msg),
ResultadoTeste::Pulado(msg) => write!(f, "PULADO: {}", msg),
}
}
}
/// Trait com Template Method para testes
trait CasoTeste {
/// Template Method — NÃO sobrescreva
fn executar(&mut self) -> ResultadoTeste {
println!("\n--- Teste: {} ---", self.nome());
// Verificar pré-condição
if let Some(motivo) = self.pular_se() {
return ResultadoTeste::Pulado(motivo);
}
// Setup
println!(" [setup]");
if let Err(e) = self.setup() {
return ResultadoTeste::Falhou(format!("Falha no setup: {}", e));
}
// Execução do teste
println!(" [executando]");
let resultado = self.testar();
// Teardown (sempre executa, mesmo se o teste falhou)
println!(" [teardown]");
if let Err(e) = self.teardown() {
println!(" [AVISO] Falha no teardown: {}", e);
}
println!(" [resultado] {}", resultado);
resultado
}
// Pontos de customização
fn nome(&self) -> &str;
fn testar(&mut self) -> ResultadoTeste;
// Hooks opcionais
fn setup(&mut self) -> Result<(), String> { Ok(()) }
fn teardown(&mut self) -> Result<(), String> { Ok(()) }
fn pular_se(&self) -> Option<String> { None }
}
/// Teste concreto: verificar cálculo
struct TesteCalculo {
resultado: Option<f64>,
}
impl CasoTeste for TesteCalculo {
fn nome(&self) -> &str { "Cálculo de área do círculo" }
fn setup(&mut self) -> Result<(), String> {
self.resultado = None;
println!(" Inicializando calculadora...");
Ok(())
}
fn testar(&mut self) -> ResultadoTeste {
let raio = 5.0;
let area = std::f64::consts::PI * raio * raio;
self.resultado = Some(area);
let esperado = 78.5398;
if (area - esperado).abs() < 0.001 {
ResultadoTeste::Passou
} else {
ResultadoTeste::Falhou(format!(
"Esperado ~{}, obtido {}", esperado, area
))
}
}
}
/// Teste concreto: verificar conexão (simulado)
struct TesteConexao {
conectado: bool,
}
impl CasoTeste for TesteConexao {
fn nome(&self) -> &str { "Conexão com banco de dados" }
fn setup(&mut self) -> Result<(), String> {
println!(" Conectando ao banco...");
self.conectado = true;
Ok(())
}
fn testar(&mut self) -> ResultadoTeste {
if self.conectado {
ResultadoTeste::Passou
} else {
ResultadoTeste::Falhou("Não conectado".to_string())
}
}
fn teardown(&mut self) -> Result<(), String> {
println!(" Desconectando...");
self.conectado = false;
Ok(())
}
}
/// Teste concreto: teste que deve ser pulado
struct TestePulaCondicional;
impl CasoTeste for TestePulaCondicional {
fn nome(&self) -> &str { "Funcionalidade experimental" }
fn pular_se(&self) -> Option<String> {
// Simula uma condição de pulo
Some("Funcionalidade em desenvolvimento".to_string())
}
fn testar(&mut self) -> ResultadoTeste {
unreachable!("Este teste não deveria executar")
}
}
/// Runner de testes
fn executar_suite(testes: &mut [Box<dyn CasoTeste>]) {
let mut passou = 0;
let mut falhou = 0;
let mut pulou = 0;
for teste in testes.iter_mut() {
match teste.executar() {
ResultadoTeste::Passou => passou += 1,
ResultadoTeste::Falhou(_) => falhou += 1,
ResultadoTeste::Pulado(_) => pulou += 1,
}
}
println!("\n==========================================");
println!("Resumo: {} passaram, {} falharam, {} pulados",
passou, falhou, pulou);
println!("==========================================");
}
fn main() {
let mut suite: Vec<Box<dyn CasoTeste>> = vec![
Box::new(TesteCalculo { resultado: None }),
Box::new(TesteConexao { conectado: false }),
Box::new(TestePulaCondicional),
];
executar_suite(&mut suite);
}
Quando Usar
- Algoritmo com passos fixos mas lógica variável: O fluxo é o mesmo, mas detalhes mudam por formato/tipo
- Framework / biblioteca: Definir o esqueleto que usuários customizam
- Processamento de dados: ETL (Extract-Transform-Load) com fontes variáveis
- Testes: Setup/teardown com lógica de teste customizável
- Relatórios: Estrutura fixa (cabeçalho, corpo, rodapé) com conteúdo variável
Quando NÃO Usar
- Algoritmo totalmente diferente: Se os passos mudam entre implementações, não há template a reusar
- Apenas um passo customizável: Se só o parsing muda, uma closure ou Strategy é mais simples
- Muitos pontos de customização: Se a maioria dos métodos é sobrescrita, a trait se torna uma interface pura (Strategy)
- Composição preferível: Se os passos são independentes, compose funções em vez de herdar template
Variações em Rust
Template com tipos genéricos associados
/// Template method com tipo de saída configurável
trait Exportador {
type Saida;
type Erro: fmt::Display;
/// Template method
fn exportar(&self, dados: &[Registro]) -> Result<Self::Saida, Self::Erro> {
let filtrados = self.filtrar(dados);
let formatados = self.formatar(&filtrados)?;
self.finalizar(formatados)
}
fn filtrar<'a>(&self, dados: &'a [Registro]) -> Vec<&'a Registro> {
dados.iter().collect() // Default: todos os registros
}
fn formatar(&self, dados: &[&Registro]) -> Result<Self::Saida, Self::Erro>;
fn finalizar(&self, saida: Self::Saida) -> Result<Self::Saida, Self::Erro> {
Ok(saida) // Default: retorna como está
}
}
Template com closures (mais flexível)
/// Builder de pipeline com closures configuráveis
struct PipelineProcessamento<T> {
pre_processamento: Box<dyn Fn(&str) -> String>,
processamento: Box<dyn Fn(&str) -> Result<Vec<T>, String>>,
pos_processamento: Box<dyn Fn(Vec<T>) -> Vec<T>>,
}
impl<T> PipelineProcessamento<T> {
fn novo<F>(processamento: F) -> Self
where
F: Fn(&str) -> Result<Vec<T>, String> + 'static,
{
PipelineProcessamento {
pre_processamento: Box::new(|s| s.trim().to_string()),
processamento: Box::new(processamento),
pos_processamento: Box::new(|v| v),
}
}
fn com_pre_processamento(mut self, f: impl Fn(&str) -> String + 'static) -> Self {
self.pre_processamento = Box::new(f);
self
}
fn com_pos_processamento(mut self, f: impl Fn(Vec<T>) -> Vec<T> + 'static) -> Self {
self.pos_processamento = Box::new(f);
self
}
fn executar(&self, entrada: &str) -> Result<Vec<T>, String> {
let preparado = (self.pre_processamento)(entrada);
let resultado = (self.processamento)(&preparado)?;
Ok((self.pos_processamento)(resultado))
}
}
Padrões Relacionados
- Strategy: Strategy troca o algoritmo inteiro; Template Method troca apenas passos do algoritmo
- Factory: Factory Methods frequentemente usam Template Method para definir o fluxo de criação
- Builder: Builder define passos de construção; Template Method define passos de processamento
- Chain of Responsibility: Ambos processam em etapas; Chain permite interromper, Template segue o fluxo completo
Conclusão
O Template Method em Rust se implementa naturalmente com traits e métodos default. A trait define o esqueleto do algoritmo como um método default que chama métodos abstratos (pontos de customização) e hooks (métodos com implementação padrão). O compilador garante que todos os pontos obrigatórios sejam implementados, e os hooks permitem personalização opcional sem código boilerplate. Esta abordagem é mais segura e flexível que herança de classes em OOP, pois evita problemas como o “frágil base class” e permite que um tipo implemente múltiplas traits template simultaneamente.