Facade em Rust

O padrao Facade em Rust: sistema de modulos como fachada natural, pub vs pub(crate), simplificacao de subsistemas complexos e exemplo pratico de fachada de banco de dados.

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.