O Type-State Pattern é um padrão idiomático do Rust que codifica estados como parâmetros de tipo genéricos, de modo que transições inválidas se tornam erros de compilação. Diferente do State Pattern clássico (que usa enums e valida transições em runtime), o Type-State move toda a lógica de validação de estado para o sistema de tipos.
A ideia central é simples e poderosa: se um objeto está no estado A, apenas métodos válidos para o estado A estão disponíveis. Tentar chamar um método do estado B resulta em erro de compilação, não em erro de runtime. O compilador se torna o guardião da máquina de estados.
Problema
Considere uma conexão HTTP que precisa passar por etapas: primeiro conectar, depois autenticar, só então enviar requisições. Em uma API convencional:
// PERIGOSO: nada impede o uso incorreto
struct ConexaoHttp {
url: String,
conectado: bool,
autenticado: bool,
token: Option<String>,
}
impl ConexaoHttp {
fn enviar(&self, dados: &str) -> Result<String, String> {
// Verificações em RUNTIME — descobertas só durante execução
if !self.conectado {
return Err("Não conectado!".to_string());
}
if !self.autenticado {
return Err("Não autenticado!".to_string());
}
Ok(format!("Enviado: {}", dados))
}
}
O usuário pode chamar enviar() sem conectar ou autenticar. O erro só aparece em runtime, possivelmente em produção.
Solução em Rust
Type-State com marcadores de tipo
use std::marker::PhantomData;
/// Marcadores de estado — tipos de tamanho zero (ZST)
/// Não ocupam memória, existem apenas no sistema de tipos
struct Desconectada;
struct Conectada;
struct Autenticada;
/// Conexão HTTP com estado codificado no tipo
struct ConexaoHttp<Estado> {
url: String,
headers: Vec<(String, String)>,
_estado: PhantomData<Estado>,
}
/// Métodos disponíveis APENAS quando Desconectada
impl ConexaoHttp<Desconectada> {
/// Cria uma nova conexão (ainda não conectada)
fn nova(url: &str) -> Self {
println!("[NOVA] Conexão criada para {}", url);
ConexaoHttp {
url: url.to_string(),
headers: Vec::new(),
_estado: PhantomData,
}
}
/// Conecta ao servidor — transforma Desconectada em Conectada
/// Note: consome self (ownership) e retorna novo tipo!
fn conectar(self) -> Result<ConexaoHttp<Conectada>, String> {
println!("[CONECTAR] Estabelecendo conexão com {}...", self.url);
// Simulando conexão bem-sucedida
Ok(ConexaoHttp {
url: self.url,
headers: self.headers,
_estado: PhantomData,
})
}
}
/// Métodos disponíveis APENAS quando Conectada
impl ConexaoHttp<Conectada> {
/// Autentica com credenciais — transforma Conectada em Autenticada
fn autenticar(mut self, usuario: &str, senha: &str) -> Result<ConexaoHttp<Autenticada>, String> {
println!("[AUTH] Autenticando como {}...", usuario);
// Simulando autenticação
if usuario.is_empty() || senha.is_empty() {
return Err("Credenciais vazias".to_string());
}
self.headers.push((
"Authorization".to_string(),
format!("Bearer token_{}_{}", usuario, senha.len()),
));
Ok(ConexaoHttp {
url: self.url,
headers: self.headers,
_estado: PhantomData,
})
}
/// Autenticação com token — transforma Conectada em Autenticada
fn autenticar_com_token(mut self, token: &str) -> ConexaoHttp<Autenticada> {
println!("[AUTH] Autenticando com token...");
self.headers.push((
"Authorization".to_string(),
format!("Bearer {}", token),
));
ConexaoHttp {
url: self.url,
headers: self.headers,
_estado: PhantomData,
}
}
/// Desconecta — volta ao estado Desconectada
fn desconectar(self) -> ConexaoHttp<Desconectada> {
println!("[DESCONECTAR] Conexão fechada");
ConexaoHttp {
url: self.url,
headers: Vec::new(),
_estado: PhantomData,
}
}
}
/// Métodos disponíveis APENAS quando Autenticada
impl ConexaoHttp<Autenticada> {
/// Envia uma requisição GET — só funciona quando autenticado!
fn get(&self, caminho: &str) -> Result<String, String> {
println!("[GET] {} {}", self.url, caminho);
Ok(format!("200 OK - {}{}", self.url, caminho))
}
/// Envia uma requisição POST
fn post(&self, caminho: &str, corpo: &str) -> Result<String, String> {
println!("[POST] {} {} - corpo: {}B", self.url, caminho, corpo.len());
Ok(format!("201 Created - {}{}", self.url, caminho))
}
/// Fecha a conexão e volta ao estado Desconectada
fn desconectar(self) -> ConexaoHttp<Desconectada> {
println!("[DESCONECTAR] Sessão encerrada");
ConexaoHttp {
url: self.url,
headers: Vec::new(),
_estado: PhantomData,
}
}
}
/// Métodos disponíveis em QUALQUER estado
impl<Estado> ConexaoHttp<Estado> {
/// Adiciona um header customizado
fn com_header(mut self, chave: &str, valor: &str) -> Self {
self.headers.push((chave.to_string(), valor.to_string()));
self
}
/// Retorna a URL da conexão
fn url(&self) -> &str {
&self.url
}
}
fn main() {
// Fluxo correto: Nova -> Conectada -> Autenticada -> usar
let resposta = ConexaoHttp::nova("https://api.exemplo.com")
.com_header("Accept", "application/json")
.conectar()
.expect("Falha ao conectar")
.autenticar("admin", "senha123")
.expect("Falha ao autenticar")
.get("/usuarios")
.expect("Falha na requisição");
println!("Resposta: {}", resposta);
// ============================================================
// Os exemplos abaixo NÃO COMPILAM — segurança em compilação!
// ============================================================
// ERRO: Desconectada não tem método get()
// let conn = ConexaoHttp::nova("https://api.exemplo.com");
// conn.get("/dados"); // ERRO: method not found in ConexaoHttp<Desconectada>
// ERRO: Conectada não tem método post()
// let conn = ConexaoHttp::nova("https://api.exemplo.com")
// .conectar().unwrap();
// conn.post("/dados", "{}"); // ERRO: method not found in ConexaoHttp<Conectada>
// ERRO: Autenticada não tem método conectar()
// let conn = ConexaoHttp::nova("https://api.exemplo.com")
// .conectar().unwrap()
// .autenticar("admin", "123").unwrap();
// conn.conectar(); // ERRO: method not found in ConexaoHttp<Autenticada>
}
Diagrama
Type-State: Estados como Tipos no Sistema de Tipos do Rust
┌─────────────────────────┐
│ ConexaoHttp<Desconectada> │
│─────────────────────────│
│ + nova() │
│ + conectar() ──────────────┐
│ + com_header() │ │ consome self
│ │ │ retorna novo tipo
│ ✗ get() — NÃO EXISTE │ │
│ ✗ post() — NÃO EXISTE │ │
└─────────────────────────┘ │
▼
┌─────────────────────────┐
│ ConexaoHttp<Conectada> │
│─────────────────────────│
│ + autenticar() ────────────┐
│ + autenticar_com_token()│ │ consome self
│ + desconectar() │ │ retorna novo tipo
│ + com_header() │ │
│ │ │
│ ✗ get() — NÃO EXISTE │ │
│ ✗ post() — NÃO EXISTE │ │
└─────────────────────────┘ │
▼
┌─────────────────────────┐
│ ConexaoHttp<Autenticada> │
│─────────────────────────│
│ + get() │
│ + post() │
│ + desconectar() │
│ + com_header() │
│ │
│ ✗ conectar() — NÃO EXISTE │
│ ✗ autenticar()— NÃO EXISTE │
└─────────────────────────┘
PhantomData — Marcador de tamanho zero:
struct Desconectada; // size = 0 bytes
struct Conectada; // size = 0 bytes
struct Autenticada; // size = 0 bytes
PhantomData<Estado> // size = 0 bytes
ConexaoHttp<Desconectada> e ConexaoHttp<Autenticada>
têm EXATAMENTE o mesmo tamanho em memória!
Exemplo do Mundo Real
Um builder de consulta SQL com type-state garantindo a ordem correta das clausulas:
use std::marker::PhantomData;
/// Estados da construção da query
struct SemTabela;
struct ComTabela;
struct ComFiltro;
struct Pronta;
/// Builder de consulta SQL com type-state
struct QueryBuilder<Estado> {
tabela: String,
colunas: Vec<String>,
filtros: Vec<String>,
ordenacao: Option<String>,
limite: Option<usize>,
_estado: PhantomData<Estado>,
}
/// Ponto de entrada: ainda sem tabela definida
impl QueryBuilder<SemTabela> {
fn selecionar() -> Self {
QueryBuilder {
tabela: String::new(),
colunas: Vec::new(),
filtros: Vec::new(),
ordenacao: None,
limite: None,
_estado: PhantomData,
}
}
/// Define a tabela — obrigatório antes de qualquer outra coisa
fn de(self, tabela: &str) -> QueryBuilder<ComTabela> {
QueryBuilder {
tabela: tabela.to_string(),
colunas: self.colunas,
filtros: self.filtros,
ordenacao: self.ordenacao,
limite: self.limite,
_estado: PhantomData,
}
}
}
/// Métodos disponíveis após definir tabela
impl QueryBuilder<ComTabela> {
/// Adiciona coluna à seleção
fn coluna(mut self, col: &str) -> Self {
self.colunas.push(col.to_string());
self
}
/// Adiciona todas as colunas
fn todas_colunas(mut self) -> Self {
self.colunas = vec!["*".to_string()];
self
}
/// Adiciona um filtro WHERE
fn onde(self, condicao: &str) -> QueryBuilder<ComFiltro> {
let mut filtros = self.filtros;
filtros.push(condicao.to_string());
QueryBuilder {
tabela: self.tabela,
colunas: self.colunas,
filtros,
ordenacao: self.ordenacao,
limite: self.limite,
_estado: PhantomData,
}
}
/// Constrói a query sem filtros
fn construir(self) -> String {
let colunas = if self.colunas.is_empty() {
"*".to_string()
} else {
self.colunas.join(", ")
};
let mut sql = format!("SELECT {} FROM {}", colunas, self.tabela);
if let Some(ord) = &self.ordenacao {
sql.push_str(&format!(" ORDER BY {}", ord));
}
if let Some(lim) = self.limite {
sql.push_str(&format!(" LIMIT {}", lim));
}
sql
}
}
/// Métodos disponíveis após adicionar filtro
impl QueryBuilder<ComFiltro> {
/// Adiciona filtro AND
fn e(mut self, condicao: &str) -> Self {
self.filtros.push(condicao.to_string());
self
}
/// Adiciona ordenação
fn ordenar_por(mut self, coluna: &str) -> Self {
self.ordenacao = Some(coluna.to_string());
self
}
/// Define limite de resultados
fn limitar(mut self, n: usize) -> Self {
self.limite = Some(n);
self
}
/// Constrói a query SQL final
fn construir(self) -> String {
let colunas = if self.colunas.is_empty() {
"*".to_string()
} else {
self.colunas.join(", ")
};
let filtros = self.filtros.join(" AND ");
let mut sql = format!(
"SELECT {} FROM {} WHERE {}",
colunas, self.tabela, filtros
);
if let Some(ord) = &self.ordenacao {
sql.push_str(&format!(" ORDER BY {}", ord));
}
if let Some(lim) = self.limite {
sql.push_str(&format!(" LIMIT {}", lim));
}
sql
}
}
fn main() {
// Builder com type-state garante uso correto
let query = QueryBuilder::selecionar()
.de("usuarios")
.coluna("nome")
.coluna("email")
.onde("idade > 18")
.e("ativo = true")
.ordenar_por("nome ASC")
.limitar(50)
.construir();
println!("SQL: {}", query);
// SELECT nome, email FROM usuarios WHERE idade > 18 AND ativo = true ORDER BY nome ASC LIMIT 50
// Query sem filtro
let query2 = QueryBuilder::selecionar()
.de("produtos")
.todas_colunas()
.construir();
println!("SQL: {}", query2);
// SELECT * FROM produtos
// ERRO DE COMPILAÇÃO: não pode construir sem definir tabela
// QueryBuilder::selecionar().construir();
// ERRO DE COMPILAÇÃO: não pode adicionar coluna sem tabela
// QueryBuilder::selecionar().coluna("nome");
// ERRO DE COMPILAÇÃO: não pode ordenar sem filtro
// QueryBuilder::selecionar().de("t").ordenar_por("nome");
}
Quando Usar
- APIs com protocolo rígido: Quando a ordem de chamadas importa (conectar antes de enviar)
- Builders seguros: Garantir que campos obrigatórios foram preenchidos antes de
build() - Máquinas de estado críticas: Sistemas financeiros, protocolos de rede, processos legais
- Prevenção de uso incorreto: Quando o custo de usar a API errado é alto
- Documentação via tipos: Os tipos comunicam o protocolo de uso da API
Quando NÃO Usar
- Muitos estados: Se a máquina de estados tem 10+ estados, a explosão de tipos torna o código difícil de manter
- Transições dinâmicas: Se o próximo estado depende de dados em runtime, use enum-based state
- Coleções heterogêneas: Não é possível ter
Vec<ConexaoHttp<_>>com diferentes estados (cada tipo é diferente) - Simplicidade: Se a API é simples e o uso incorreto é improvável, o overhead cognitivo não compensa
Variações em Rust
Sealed traits para estados
/// Módulo selado — não pode ser implementado fora
mod estados {
/// Trait selada — apenas estados conhecidos implementam
pub trait EstadoConexao: private::Sealed {}
mod private {
pub trait Sealed {}
impl Sealed for super::Aberta {}
impl Sealed for super::Fechada {}
}
pub struct Aberta;
pub struct Fechada;
impl EstadoConexao for Aberta {}
impl EstadoConexao for Fechada {}
}
/// Agora só estados válidos podem ser usados como parâmetro
struct Conexao<E: estados::EstadoConexao> {
_estado: PhantomData<E>,
}
Type-State com múltiplos eixos
/// Combinando múltiplos eixos de estado
struct Nao;
struct Sim;
struct Formulario<TemNome, TemEmail> {
nome: Option<String>,
email: Option<String>,
_nome: PhantomData<TemNome>,
_email: PhantomData<TemEmail>,
}
impl Formulario<Nao, Nao> {
fn novo() -> Self {
Formulario {
nome: None,
email: None,
_nome: PhantomData,
_email: PhantomData,
}
}
}
impl<E> Formulario<Nao, E> {
fn com_nome(self, nome: &str) -> Formulario<Sim, E> {
Formulario {
nome: Some(nome.to_string()),
email: self.email,
_nome: PhantomData,
_email: PhantomData,
}
}
}
impl<N> Formulario<N, Nao> {
fn com_email(self, email: &str) -> Formulario<N, Sim> {
Formulario {
nome: self.nome,
email: Some(email.to_string()),
_nome: PhantomData,
_email: PhantomData,
}
}
}
/// enviar() SÓ existe quando AMBOS os campos estão preenchidos
impl Formulario<Sim, Sim> {
fn enviar(&self) -> String {
format!(
"Enviado: {} <{}>",
self.nome.as_ref().unwrap(),
self.email.as_ref().unwrap(),
)
}
}
fn main() {
// Ordem não importa — ambos os caminhos funcionam
let resultado = Formulario::novo()
.com_nome("Maria")
.com_email("maria@exemplo.com")
.enviar();
println!("{}", resultado);
let resultado2 = Formulario::novo()
.com_email("joao@exemplo.com")
.com_nome("João")
.enviar();
println!("{}", resultado2);
// ERRO: não pode enviar sem nome
// Formulario::novo().com_email("x@x.com").enviar();
// ERRO: não pode enviar sem email
// Formulario::novo().com_nome("Ana").enviar();
}
Padrões Relacionados
- State: Versão em runtime do type-state; usa enums em vez de tipos genéricos
- Builder: Builder com type-state garante que campos obrigatórios foram preenchidos
- Newtype: Os marcadores de estado são essencialmente newtypes de tamanho zero
- RAII: Type-state pode incluir Drop para cleanup automático em transições
Conclusão
O Type-State Pattern representa o auge da programação orientada a tipos em Rust. Ao codificar estados como parâmetros de tipo genéricos, movemos a validação de transições de estado do runtime para o compilador. O custo é zero em runtime (PhantomData não ocupa memória), e o benefício é imenso: estados inválidos são literalmente irrepresentáveis no código. Este padrão é particularmente valioso em APIs públicas de bibliotecas, onde o custo de uso incorreto pode ser alto. Quando bem aplicado, o Type-State transforma o compilador em um guardião incansável do protocolo da sua API.