Introducao
O Facade (Fachada) e um padrao estrutural que fornece uma interface simplificada para um subsistema complexo. Em vez de expor dezenas de classes e metodos internos, a Fachada oferece uma API limpa e minima que cobre os casos de uso mais comuns.
Em Rust, o sistema de modulos funciona como uma Fachada natural. Com pub, pub(crate),
pub(super) e re-exportacoes via pub use, voce controla exatamente o que e visivel
para o mundo exterior, escondendo a complexidade interna. Esse padrao e fundamental para
projetar APIs de bibliotecas ergonomicas.
Problema
Voce esta construindo uma aplicacao que precisa interagir com um banco de dados. Isso envolve multiplos subsistemas: pool de conexoes, migracao de esquema, construcao de queries, gerenciamento de transacoes e cache de resultados. Cada subsistema tem sua propria API com dezenas de configuracoes.
// Sem Facade: o usuario precisa conhecer e coordenar tudo manualmente
fn main() {
// 1. Configurar pool de conexoes
let pool_config = PoolConfig::new()
.max_size(10)
.min_idle(2)
.max_lifetime(Duration::from_secs(1800))
.idle_timeout(Duration::from_secs(600))
.connection_timeout(Duration::from_secs(30));
let pool = ConnectionPool::new("postgres://localhost/db", pool_config);
// 2. Rodar migracoes
let migrator = Migrator::new(&pool);
migrator.set_directory("./migrations");
migrator.run_pending()?;
// 3. Construir query
let query = QueryBuilder::new()
.table("usuarios")
.select(&["id", "nome", "email"])
.where_clause("ativo = $1", &[&true])
.order_by("nome", Order::Asc)
.limit(50);
// 4. Executar com transacao
let conn = pool.get()?;
let tx = conn.begin_transaction()?;
let result = tx.execute(query)?;
tx.commit()?;
// COMPLEXO DEMAIS para operacoes simples!
}
Solucao em Rust
Facade para Banco de Dados
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
// ================================================================
// Subsistema 1: Pool de Conexoes
// ================================================================
mod pool {
use std::sync::{Arc, Mutex};
#[derive(Debug)]
pub(crate) struct ConexaoInterna {
pub url: String,
pub ativa: bool,
id: u32,
}
pub(crate) struct PoolConexoes {
conexoes: Vec<ConexaoInterna>,
max_size: usize,
url: String,
proxima_id: u32,
}
impl PoolConexoes {
pub fn new(url: &str, max_size: usize) -> Self {
println!("[Pool] Criando pool com max {} conexoes para {}", max_size, url);
Self {
conexoes: Vec::new(),
max_size,
url: url.to_string(),
proxima_id: 0,
}
}
pub fn obter(&mut self) -> Result<&mut ConexaoInterna, String> {
// Tenta reusar conexao existente
if let Some(conn) = self.conexoes.iter_mut().find(|c| !c.ativa) {
conn.ativa = true;
println!("[Pool] Reusando conexao #{}", conn.id);
return Ok(conn);
}
// Cria nova conexao se possivel
if self.conexoes.len() < self.max_size {
let id = self.proxima_id;
self.proxima_id += 1;
self.conexoes.push(ConexaoInterna {
url: self.url.clone(),
ativa: true,
id,
});
println!("[Pool] Nova conexao #{} criada", id);
return Ok(self.conexoes.last_mut().unwrap());
}
Err("Pool esgotado - todas as conexoes em uso".to_string())
}
pub fn liberar_todas(&mut self) {
for conn in &mut self.conexoes {
conn.ativa = false;
}
println!("[Pool] Todas as conexoes liberadas");
}
pub fn estatisticas(&self) -> (usize, usize) {
let ativas = self.conexoes.iter().filter(|c| c.ativa).count();
(ativas, self.conexoes.len())
}
}
}
// ================================================================
// Subsistema 2: Migracoes de Esquema
// ================================================================
mod migracoes {
#[derive(Debug)]
pub(crate) struct Migracao {
pub versao: u32,
pub nome: String,
pub sql: String,
pub aplicada: bool,
}
pub(crate) struct GerenciadorMigracoes {
migracoes: Vec<Migracao>,
versao_atual: u32,
}
impl GerenciadorMigracoes {
pub fn new() -> Self {
Self {
migracoes: Vec::new(),
versao_atual: 0,
}
}
pub fn adicionar(&mut self, nome: &str, sql: &str) {
let versao = self.migracoes.len() as u32 + 1;
self.migracoes.push(Migracao {
versao,
nome: nome.to_string(),
sql: sql.to_string(),
aplicada: false,
});
}
pub fn executar_pendentes(&mut self) -> Result<Vec<String>, String> {
let mut aplicadas = Vec::new();
for m in &mut self.migracoes {
if !m.aplicada && m.versao > self.versao_atual {
println!(
"[Migracoes] Aplicando v{}: {}",
m.versao, m.nome
);
// Em producao, executaria o SQL aqui
m.aplicada = true;
self.versao_atual = m.versao;
aplicadas.push(format!("v{}: {}", m.versao, m.nome));
}
}
if aplicadas.is_empty() {
println!("[Migracoes] Nenhuma migracao pendente");
}
Ok(aplicadas)
}
pub fn versao_atual(&self) -> u32 {
self.versao_atual
}
}
}
// ================================================================
// Subsistema 3: Query Builder
// ================================================================
mod query {
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Ordem {
Asc,
Desc,
}
#[derive(Debug)]
pub(crate) struct ConstrutorQuery {
tabela: String,
colunas: Vec<String>,
condicoes: Vec<String>,
ordem: Option<(String, Ordem)>,
limite: Option<u32>,
}
/// Resultado de uma consulta
#[derive(Debug)]
pub struct ResultadoQuery {
pub colunas: Vec<String>,
pub linhas: Vec<HashMap<String, String>>,
}
impl ConstrutorQuery {
pub fn selecionar(tabela: &str) -> Self {
Self {
tabela: tabela.to_string(),
colunas: vec!["*".to_string()],
condicoes: Vec::new(),
ordem: None,
limite: None,
}
}
pub fn colunas(mut self, cols: &[&str]) -> Self {
self.colunas = cols.iter().map(|c| c.to_string()).collect();
self
}
pub fn filtro(mut self, condicao: &str) -> Self {
self.condicoes.push(condicao.to_string());
self
}
pub fn ordenar(mut self, coluna: &str, ordem: Ordem) -> Self {
self.ordem = Some((coluna.to_string(), ordem));
self
}
pub fn limite(mut self, n: u32) -> Self {
self.limite = Some(n);
self
}
pub fn to_sql(&self) -> String {
let mut sql = format!(
"SELECT {} FROM {}",
self.colunas.join(", "),
self.tabela
);
if !self.condicoes.is_empty() {
sql.push_str(" WHERE ");
sql.push_str(&self.condicoes.join(" AND "));
}
if let Some((col, ord)) = &self.ordem {
let dir = match ord {
Ordem::Asc => "ASC",
Ordem::Desc => "DESC",
};
sql.push_str(&format!(" ORDER BY {} {}", col, dir));
}
if let Some(lim) = self.limite {
sql.push_str(&format!(" LIMIT {}", lim));
}
sql
}
}
}
// ================================================================
// FACADE: Interface Simplificada
// ================================================================
/// Erro unificado da fachada
#[derive(Debug)]
pub enum ErroDb {
Conexao(String),
Migracao(String),
Consulta(String),
}
impl std::fmt::Display for ErroDb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ErroDb::Conexao(msg) => write!(f, "Erro de conexao: {}", msg),
ErroDb::Migracao(msg) => write!(f, "Erro de migracao: {}", msg),
ErroDb::Consulta(msg) => write!(f, "Erro de consulta: {}", msg),
}
}
}
/// A Fachada: interface simples para todo o subsistema de banco de dados
pub struct Database {
pool: pool::PoolConexoes,
migracoes: migracoes::GerenciadorMigracoes,
}
impl Database {
/// Cria e conecta ao banco de dados com configuracoes padrao
pub fn conectar(url: &str) -> Result<Self, ErroDb> {
let pool = pool::PoolConexoes::new(url, 10);
Ok(Self {
pool,
migracoes: migracoes::GerenciadorMigracoes::new(),
})
}
/// Conecta com tamanho de pool customizado
pub fn conectar_com_pool(url: &str, pool_size: usize) -> Result<Self, ErroDb> {
let pool = pool::PoolConexoes::new(url, pool_size);
Ok(Self {
pool,
migracoes: migracoes::GerenciadorMigracoes::new(),
})
}
/// Adiciona e executa migracoes de uma so vez
pub fn migrar(&mut self, migracoes: &[(&str, &str)]) -> Result<(), ErroDb> {
for (nome, sql) in migracoes {
self.migracoes.adicionar(nome, sql);
}
self.migracoes
.executar_pendentes()
.map_err(|e| ErroDb::Migracao(e))?;
Ok(())
}
/// Consulta simples: seleciona todos os registros de uma tabela
pub fn buscar_todos(&mut self, tabela: &str) -> Result<String, ErroDb> {
let _conn = self.pool.obter().map_err(|e| ErroDb::Conexao(e))?;
let sql = query::ConstrutorQuery::selecionar(tabela).to_sql();
println!("[Database] Executando: {}", sql);
self.pool.liberar_todas();
Ok(sql)
}
/// Consulta com filtros
pub fn buscar_filtrado(
&mut self,
tabela: &str,
colunas: &[&str],
filtro: &str,
limite: Option<u32>,
) -> Result<String, ErroDb> {
let _conn = self.pool.obter().map_err(|e| ErroDb::Conexao(e))?;
let mut q = query::ConstrutorQuery::selecionar(tabela)
.colunas(colunas)
.filtro(filtro);
if let Some(lim) = limite {
q = q.limite(lim);
}
let sql = q.to_sql();
println!("[Database] Executando: {}", sql);
self.pool.liberar_todas();
Ok(sql)
}
/// Retorna informacoes sobre o estado do banco
pub fn info(&self) -> DatabaseInfo {
let (ativas, total) = self.pool.estatisticas();
DatabaseInfo {
conexoes_ativas: ativas,
conexoes_total: total,
versao_esquema: self.migracoes.versao_atual(),
}
}
}
#[derive(Debug)]
pub struct DatabaseInfo {
pub conexoes_ativas: usize,
pub conexoes_total: usize,
pub versao_esquema: u32,
}
fn main() {
// A FACADE esconde toda a complexidade!
// Compare com o "Problema" no inicio do artigo.
// 1. Conectar (uma linha!)
let mut db = Database::conectar("postgres://localhost/meu_app")
.expect("Falha ao conectar");
// 2. Migrar (uma chamada!)
db.migrar(&[
("criar_usuarios", "CREATE TABLE usuarios (id SERIAL, nome TEXT, email TEXT)"),
("criar_pedidos", "CREATE TABLE pedidos (id SERIAL, usuario_id INT, valor DECIMAL)"),
("adicionar_indice", "CREATE INDEX idx_email ON usuarios(email)"),
])
.expect("Falha nas migracoes");
// 3. Consultar (simples e direto!)
let sql = db
.buscar_todos("usuarios")
.expect("Falha na consulta");
println!("SQL gerado: {}\n", sql);
let sql = db
.buscar_filtrado(
"pedidos",
&["id", "valor", "usuario_id"],
"valor > 100.00",
Some(20),
)
.expect("Falha na consulta");
println!("SQL gerado: {}\n", sql);
// 4. Verificar estado
let info = db.info();
println!("Estado do banco: {:?}", info);
}
Diagrama
SEM FACADE:
+------------------+
Codigo do +-----------+----->| PoolConexoes |
Usuario | | +------------------+
| | | GerMigracoes |
(precisa | +----->+------------------+
conhecer | | | ConstrutorQuery |
TUDO) +-----------+----->+------------------+
| | Transacao |
+----->+------------------+
| | Cache |
+----->+------------------+
COM FACADE:
+----------+ +------------------+
Codigo do | | | PoolConexoes |
Usuario ---->| Database |------>+------------------+
| (Facade) | | GerMigracoes |
(API | |------>+------------------+
simples) | | | ConstrutorQuery |
+----------+------>+------------------+
O usuario so conhece a interface de Database.
Os subsistemas internos sao pub(crate) — invisiveis externamente.
Exemplo do Mundo Real
Fachada para um sistema de envio de notificacoes multi-canal:
use std::collections::HashMap;
/// Resultado do envio de uma notificacao
#[derive(Debug)]
pub struct ResultadoEnvio {
pub canal: String,
pub sucesso: bool,
pub mensagem: String,
}
// Subsistema: Email
mod email {
pub(crate) struct ClienteEmail {
smtp_host: String,
}
impl ClienteEmail {
pub fn new(host: &str) -> Self {
Self { smtp_host: host.to_string() }
}
pub fn enviar(&self, para: &str, assunto: &str, corpo: &str) -> Result<(), String> {
println!(
"[Email] Enviando para {} via {}: '{}' - {}",
para, self.smtp_host, assunto, corpo
);
Ok(())
}
}
}
// Subsistema: SMS
mod sms {
pub(crate) struct ClienteSms {
api_key: String,
}
impl ClienteSms {
pub fn new(api_key: &str) -> Self {
Self { api_key: api_key.to_string() }
}
pub fn enviar(&self, telefone: &str, mensagem: &str) -> Result<(), String> {
println!(
"[SMS] Enviando para {}: '{}' (key: {}...)",
telefone,
mensagem,
&self.api_key[..6]
);
Ok(())
}
}
}
// Subsistema: Push Notification
mod push {
pub(crate) struct ClientePush {
app_id: String,
}
impl ClientePush {
pub fn new(app_id: &str) -> Self {
Self { app_id: app_id.to_string() }
}
pub fn enviar(&self, device_token: &str, titulo: &str, corpo: &str) -> Result<(), String> {
println!(
"[Push] App {}: Enviando para device {}: '{}' - {}",
self.app_id, device_token, titulo, corpo
);
Ok(())
}
}
}
/// FACADE: Interface unificada para notificacoes
pub struct Notificador {
email: email::ClienteEmail,
sms: sms::ClienteSms,
push: push::ClientePush,
}
/// Destinatario com informacoes de contato
pub struct Destinatario {
pub nome: String,
pub email: Option<String>,
pub telefone: Option<String>,
pub device_token: Option<String>,
}
impl Notificador {
/// Cria o notificador configurando todos os subsistemas
pub fn new(smtp_host: &str, sms_key: &str, push_app_id: &str) -> Self {
Self {
email: email::ClienteEmail::new(smtp_host),
sms: sms::ClienteSms::new(sms_key),
push: push::ClientePush::new(push_app_id),
}
}
/// Envia notificacao por TODOS os canais disponiveis do destinatario
pub fn notificar(
&self,
dest: &Destinatario,
titulo: &str,
mensagem: &str,
) -> Vec<ResultadoEnvio> {
let mut resultados = Vec::new();
if let Some(email) = &dest.email {
let resultado = self.email.enviar(email, titulo, mensagem);
resultados.push(ResultadoEnvio {
canal: "email".to_string(),
sucesso: resultado.is_ok(),
mensagem: resultado.err().unwrap_or_else(|| "Enviado".to_string()),
});
}
if let Some(telefone) = &dest.telefone {
let msg_curta = if mensagem.len() > 160 {
format!("{}...", &mensagem[..157])
} else {
mensagem.to_string()
};
let resultado = self.sms.enviar(telefone, &msg_curta);
resultados.push(ResultadoEnvio {
canal: "sms".to_string(),
sucesso: resultado.is_ok(),
mensagem: resultado.err().unwrap_or_else(|| "Enviado".to_string()),
});
}
if let Some(token) = &dest.device_token {
let resultado = self.push.enviar(token, titulo, mensagem);
resultados.push(ResultadoEnvio {
canal: "push".to_string(),
sucesso: resultado.is_ok(),
mensagem: resultado.err().unwrap_or_else(|| "Enviado".to_string()),
});
}
resultados
}
/// Atalho: envia apenas por email
pub fn enviar_email(&self, para: &str, assunto: &str, corpo: &str) -> Result<(), String> {
self.email.enviar(para, assunto, corpo)
}
/// Atalho: envia apenas por SMS
pub fn enviar_sms(&self, telefone: &str, mensagem: &str) -> Result<(), String> {
self.sms.enviar(telefone, mensagem)
}
}
fn main() {
// A facade configura todos os subsistemas de uma vez
let notificador = Notificador::new(
"smtp.exemplo.com",
"sk_sms_abcdef123456",
"app-push-xyz",
);
let destinatario = Destinatario {
nome: "Maria Silva".to_string(),
email: Some("maria@exemplo.com".to_string()),
telefone: Some("+5511999887766".to_string()),
device_token: Some("fcm_token_abc123".to_string()),
};
// Uma unica chamada envia por todos os canais disponiveis
println!("=== Notificando {} ===", destinatario.nome);
let resultados = notificador.notificar(
&destinatario,
"Pedido Confirmado",
"Seu pedido #12345 foi confirmado e sera entregue em 2 dias uteis.",
);
println!("\n=== Resultados ===");
for r in resultados {
println!(
" {}: {} ({})",
r.canal,
if r.sucesso { "OK" } else { "FALHA" },
r.mensagem
);
}
}
Quando Usar
- Bibliotecas com muitas funcionalidades - exponha uma API simples no prelude
- Subsistemas complexos que precisam ser coordenados (banco + cache + migracoes)
- Desacoplamento entre camadas da aplicacao
- APIs publicas de crates - use re-exportacoes para esconder detalhes internos
- Integracao de multiplos servicos externos em uma interface unificada
Quando NAO Usar
- Quando a complexidade nao justifica a camada extra de abstracao
- Facade que apenas delega sem simplificar nada (wrapper inuteis)
- Quando os usuarios precisam de controle fino sobre os subsistemas
- API em evolucao rapida - a facade pode ficar desatualizada rapidamente
Variacoes em Rust
1. Facade via pub use (re-exportacao)
// lib.rs - a forma mais idiomatica em Rust
mod pool;
mod migracoes;
mod query;
// Re-exporta apenas o que e publico
pub use pool::Pool;
pub use query::{Query, ResultadoQuery};
// migracoes fica totalmente oculto
2. Facade com prelude
// Padrao comum em crates Rust
pub mod prelude {
pub use crate::Database;
pub use crate::ErroDb;
pub use crate::ResultadoQuery;
// Nao exporta detalhes internos
}
// O usuario importa tudo de uma vez:
// use minha_crate::prelude::*;
3. Facade com trait
/// Trait define a interface da facade
pub trait BancoFacade {
fn consultar(&self, sql: &str) -> Result<Vec<Linha>, Erro>;
fn inserir(&self, tabela: &str, dados: &Registro) -> Result<u64, Erro>;
fn migrar(&self) -> Result<(), Erro>;
}
// Diferentes implementacoes para Postgres, SQLite, etc.
// Cada uma esconde sua propria complexidade interna
Padroes Relacionados
- Adapter - Adapter torna interfaces compativeis; Facade simplifica
- Decorator - Decorator adiciona funcionalidade; Facade reduz complexidade
- Singleton - Facades frequentemente sao Singletons (um banco, um notificador)
- Builder - Builder pode ser usado para configurar a Facade
Conclusao
O Facade e um dos padroes mais naturais em Rust, gracas ao sistema de modulos com
controle de visibilidade granular (pub, pub(crate), pub(super), pub(in path)).
Ao projetar bibliotecas e modulos, pense sempre na API que o usuario precisa ver versus
a complexidade interna que deve permanecer oculta. Re-exportacoes com pub use e modulos
prelude sao as ferramentas idiomaticas de Rust para criar fachadas limpas. Uma boa
Facade nao apenas simplifica, ela guia o usuario pelo caminho mais seguro e eficiente
de usar seu codigo.