Módulos e Organização de Código — Estruturando Projetos Reais

Módulos e Organização de Código — Estruturando Projetos Reais

Privado por padrão, o sistema de módulos obriga a decidir o que é interface e o que é detalhe. O artigo cobre a declaração de módulos e os arquivos correspondentes, a visibilidade granular, os caminhos absolutos e relativos, e a divisão entre biblioteca e binário que torna o código testável.
Rust

14 min de leitura

 

Até agora todos os nossos exemplos viveram em um único arquivo main.rs. Isso funciona para programas pequenos, mas projetos reais têm centenas ou milhares de arquivos, dezenas de colaboradores, e código que precisa ser reutilizado em múltiplos contextos. Rust oferece um sistema de módulos robusto para organizar tudo isso de forma clara e controlada.

Neste artigo vamos entender como Rust organiza código em módulos, arquivos e crates — e como controlar o que é público e o que é privado.

O sistema de módulos de Rust

Rust organiza código em uma hierarquia de três níveis:

Crates são a unidade de compilação — um binário ou uma biblioteca. Cada projeto Cargo é um crate.

Módulos são namespaces dentro de um crate — agrupam código relacionado e controlam visibilidade.

Itens são o conteúdo dos módulos — funções, structs, enums, traits, constantes.

Essa hierarquia forma uma árvore, com a raiz sendo o arquivo principal do crate (main.rs para binários, lib.rs para bibliotecas).

Módulos inline com mod

A forma mais simples de criar um módulo é diretamente no arquivo:

mod matematica {
    pub fn somar(a: f64, b: f64) -> f64 {
        a + b
    }

    pub fn subtrair(a: f64, b: f64) -> f64 {
        a - b
    }

    // Função privada — só acessível dentro do módulo
    fn interno() -> &'static str {
        "função interna"
    }

    pub mod avancado {
        pub fn raiz_quadrada(x: f64) -> f64 {
            x.sqrt()
        }

        pub fn potencia(base: f64, exp: u32) -> f64 {
            base.powi(exp as i32)
        }
    }
}

fn main() {
    println!("{}", matematica::somar(3.0, 4.0));
    println!("{}", matematica::subtrair(10.0, 3.0));
    println!("{}", matematica::avancado::raiz_quadrada(16.0));
    println!("{}", matematica::avancado::potencia(2.0, 10));

    // matematica::interno(); // ERRO: função privada
}

Saída:

7
7
4
1024

Por padrão, tudo em Rust é privado. A palavra-chave pub torna um item público — acessível fora do módulo onde foi definido. Isso é o inverso de muitas linguagens, onde tudo é público por padrão.

Visibilidade granular

Rust oferece controle fino sobre visibilidade:

mod biblioteca {
    // Público para todos
    pub struct Livro {
        pub titulo: String,
        pub autor: String,
        preco: f64, // privado mesmo sendo em struct pública
    }

    impl Livro {
        // Construtor público — única forma de criar Livro de fora
        pub fn novo(titulo: &str, autor: &str, preco: f64) -> Self {
            Livro {
                titulo: titulo.to_string(),
                autor: autor.to_string(),
                preco,
            }
        }

        // Público
        pub fn preco_com_desconto(&self, desconto: f64) -> f64 {
            self.preco * (1.0 - desconto)
        }

        // Público apenas para o módulo pai
        pub(super) fn preco_interno(&self) -> f64 {
            self.preco
        }

        // Público apenas dentro do crate
        pub(crate) fn isbn_gerado(&self) -> String {
            format!("978-{:010}", self.titulo.len())
        }
    }

    // Privado ao módulo — não acessível fora
    fn validar_preco(preco: f64) -> bool {
        preco > 0.0
    }
}

fn main() {
    let livro = biblioteca::Livro::novo(
        "The Rust Programming Language",
        "Steve Klabnik",
        89.90,
    );

    println!("Título: {}", livro.titulo);
    println!("Autor: {}", livro.autor);
    // println!("{}", livro.preco); // ERRO: campo privado

    println!("Com 10% de desconto: R$ {:.2}",
        livro.preco_com_desconto(0.10));
    println!("ISBN: {}", livro.isbn_gerado());
}

As variantes de visibilidade disponíveis são:

  • pub — público para todos
  • pub(crate) — público dentro do crate, privado fora
  • pub(super) — público para o módulo pai
  • pub(in caminho) — público para um módulo específico
  • sem modificador — privado ao módulo atual

use — trazendo nomes ao escopo

Escrever matematica::avancado::raiz_quadrada toda vez é verboso. O use traz nomes ao escopo atual:

mod geometria {
    pub mod formas {
        pub struct Circulo {
            pub raio: f64,
        }

        pub struct Retangulo {
            pub largura: f64,
            pub altura: f64,
        }

        impl Circulo {
            pub fn area(&self) -> f64 {
                std::f64::consts::PI * self.raio * self.raio
            }
        }

        impl Retangulo {
            pub fn area(&self) -> f64 {
                self.largura * self.altura
            }
        }
    }
}

// Trazendo tipos específicos ao escopo
use geometria::formas::{Circulo, Retangulo};

// Alias para evitar conflitos de nome
use std::fmt::Display as Exibivel;

fn main() {
    let c = Circulo { raio: 5.0 };
    let r = Retangulo { largura: 4.0, altura: 6.0 };

    println!("Círculo: {:.2}", c.area());
    println!("Retângulo: {:.2}", r.area());
}

Por convenção, ao usar use com funções, importe o módulo pai — não a função diretamente. Isso deixa claro na chamada que a função não é local:

// Preferido para funções:
use std::collections;
collections::HashMap::new();

// Preferido para tipos e traits:
use std::collections::HashMap;
HashMap::new();

use com glob

O glob * importa todos os itens públicos de um módulo:

use geometria::formas::*;

Use com moderação — pode poluir o namespace e tornar difícil saber de onde cada nome vem. É aceitável em testes e em módulos prelude (convenção de bibliotecas para importar os itens mais comuns).

Módulos em arquivos separados

Em projetos reais, cada módulo vai em seu próprio arquivo. O Cargo segue uma convenção clara de estrutura de diretórios:

meu_projeto/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── matematica.rs
    └── geometria/
        ├── mod.rs   (ou geometria.rs no Rust moderno)
        ├── circulo.rs
        └── retangulo.rs

src/matematica.rs:

pub fn somar(a: f64, b: f64) -> f64 {
    a + b
}

pub fn media(valores: &[f64]) -> f64 {
    let soma: f64 = valores.iter().sum();
    soma / valores.len() as f64
}

src/geometria/circulo.rs:

pub struct Circulo {
    pub raio: f64,
}

impl Circulo {
    pub fn novo(raio: f64) -> Self {
        Circulo { raio }
    }

    pub fn area(&self) -> f64 {
        std::f64::consts::PI * self.raio * self.raio
    }

    pub fn perimetro(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.raio
    }
}

src/geometria/retangulo.rs:

pub struct Retangulo {
    pub largura: f64,
    pub altura: f64,
}

impl Retangulo {
    pub fn novo(largura: f64, altura: f64) -> Self {
        Retangulo { largura, altura }
    }

    pub fn area(&self) -> f64 {
        self.largura * self.altura
    }

    pub fn e_quadrado(&self) -> bool {
        self.largura == self.altura
    }
}

src/geometria/mod.rs (ou src/geometria.rs — Rust 2018+):

pub mod circulo;
pub mod retangulo;

// Re-exportando para facilitar uso externo
pub use circulo::Circulo;
pub use retangulo::Retangulo;

src/main.rs:

mod matematica;
mod geometria;

use geometria::{Circulo, Retangulo};

fn main() {
    let soma = matematica::somar(3.0, 4.0);
    let media = matematica::media(&[1.0, 2.0, 3.0, 4.0, 5.0]);

    println!("Soma: {soma}");
    println!("Média: {media}");

    let c = Circulo::novo(5.0);
    let r = Retangulo::novo(4.0, 6.0);

    println!("Círculo — área: {:.2}", c.area());
    println!("Retângulo — área: {:.2}", r.area());
    println!("É quadrado? {}", r.e_quadrado());
}

A declaração mod matematica; em main.rs diz ao compilador: "procure o módulo matematica em src/matematica.rs ou src/matematica/mod.rs." O arquivo é carregado automaticamente.

Bibliotecas vs binários

Um crate pode ser biblioteca, binário, ou ambos simultaneamente:

meu_projeto/
├── Cargo.toml
└── src/
    ├── main.rs     ← binário (fn main)
    └── lib.rs      ← biblioteca (sem fn main)

lib.rs é a raiz da biblioteca. Código em lib.rs pode ser usado pelo main.rs do mesmo projeto e por outros projetos que dependem do seu crate.

src/lib.rs:

pub mod calculadora;
pub mod geometria;

// Versão da biblioteca acessível por dependentes
pub const VERSAO: &str = "1.0.0";

src/main.rs:

// Usa a própria biblioteca do projeto
use meu_projeto::geometria::Circulo;
use meu_projeto::VERSAO;

fn main() {
    println!("Versão: {VERSAO}");
    let c = Circulo::novo(3.0);
    println!("Área: {:.2}", c.area());
}

Essa separação é fundamental para criar bibliotecas testáveis — os testes de integração vivem em tests/ e usam a crate como qualquer dependente externo usaria.

O arquivo Cargo.toml

Todo projeto Rust tem um Cargo.toml que define metadados e dependências:

[package]
name = "meu_projeto"
version = "0.1.0"
edition = "2021"
authors = ["Seu Nome <seu@email.com>"]
description = "Descrição do projeto"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"

[dev-dependencies]
# Dependências apenas para testes
criterion = "0.5"

[lib]
name = "meu_projeto"
path = "src/lib.rs"

[[bin]]
name = "meu_projeto"
path = "src/main.rs"

O Cargo gerencia automaticamente o download, compilação e atualização de dependências. O ecossistema de pacotes fica em crates.io — o repositório central de crates Rust.

Múltiplos binários

Um projeto pode ter múltiplos binários em src/bin/:

meu_projeto/
├── Cargo.toml
└── src/
    ├── lib.rs
    └── bin/
        ├── servidor.rs
        ├── cliente.rs
        └── migrador.rs

Cada arquivo em src/bin/ torna-se um binário separado, executável com:

cargo run --bin servidor
cargo run --bin cliente
cargo build --bin migrador

Um projeto completo organizado

Vamos ver como ficaria um projeto de gerenciamento financeiro organizado em módulos:

Estrutura:

financas/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── lib.rs
    ├── conta.rs
    ├── transacao.rs
    └── relatorio.rs

src/transacao.rs:

#[derive(Debug, Clone)]
pub enum TipoTransacao {
    Credito,
    Debito,
}

#[derive(Debug, Clone)]
pub struct Transacao {
    pub descricao: String,
    pub valor: f64,
    pub tipo: TipoTransacao,
}

impl Transacao {
    pub fn credito(descricao: &str, valor: f64) -> Self {
        Transacao {
            descricao: descricao.to_string(),
            valor,
            tipo: TipoTransacao::Credito,
        }
    }

    pub fn debito(descricao: &str, valor: f64) -> Self {
        Transacao {
            descricao: descricao.to_string(),
            valor,
            tipo: TipoTransacao::Debito,
        }
    }

    pub fn valor_liquido(&self) -> f64 {
        match self.tipo {
            TipoTransacao::Credito => self.valor,
            TipoTransacao::Debito  => -self.valor,
        }
    }
}

src/conta.rs:

use crate::transacao::Transacao;

#[derive(Debug)]
pub struct Conta {
    pub titular: String,
    pub saldo: f64,
    transacoes: Vec<Transacao>,
}

impl Conta {
    pub fn nova(titular: &str, saldo_inicial: f64) -> Self {
        Conta {
            titular: titular.to_string(),
            saldo: saldo_inicial,
            transacoes: Vec::new(),
        }
    }

    pub fn registrar(&mut self, transacao: Transacao) {
        self.saldo += transacao.valor_liquido();
        self.transacoes.push(transacao);
    }

    pub fn historico(&self) -> &[Transacao] {
        &self.transacoes
    }

    pub fn total_creditos(&self) -> f64 {
        self.transacoes.iter()
            .filter(|t| matches!(t.tipo, crate::transacao::TipoTransacao::Credito))
            .map(|t| t.valor)
            .sum()
    }

    pub fn total_debitos(&self) -> f64 {
        self.transacoes.iter()
            .filter(|t| matches!(t.tipo, crate::transacao::TipoTransacao::Debito))
            .map(|t| t.valor)
            .sum()
    }
}

src/relatorio.rs:

use crate::conta::Conta;
use crate::transacao::TipoTransacao;

pub fn exibir_extrato(conta: &Conta) {
    println!("══════════════════════════════════════");
    println!("  Extrato — {}", conta.titular);
    println!("══════════════════════════════════════");

    for t in conta.historico() {
        let simbolo = match t.tipo {
            TipoTransacao::Credito => "+",
            TipoTransacao::Debito  => "-",
        };
        println!("  [{simbolo}] {:<25} R$ {:>8.2}",
            t.descricao, t.valor);
    }

    println!("──────────────────────────────────────");
    println!("  Total créditos : R$ {:>8.2}", conta.total_creditos());
    println!("  Total débitos  : R$ {:>8.2}", conta.total_debitos());
    println!("  Saldo atual    : R$ {:>8.2}", conta.saldo);
    println!("══════════════════════════════════════");
}

src/main.rs:

mod conta;
mod transacao;
mod relatorio;

use conta::Conta;
use transacao::Transacao;
use relatorio::exibir_extrato;

fn main() {
    let mut minha_conta = Conta::nova("Ana Silva", 1000.0);

    minha_conta.registrar(Transacao::credito("Salário", 5000.0));
    minha_conta.registrar(Transacao::debito("Aluguel", 1500.0));
    minha_conta.registrar(Transacao::debito("Supermercado", 450.0));
    minha_conta.registrar(Transacao::credito("Freelance", 800.0));
    minha_conta.registrar(Transacao::debito("Conta de luz", 180.0));
    minha_conta.registrar(Transacao::debito("Internet", 99.90));

    exibir_extrato(&minha_conta);
}

Saída:

══════════════════════════════════════
  Extrato — Ana Silva
══════════════════════════════════════
  [+] Salário                   R$  5000.00
  [-] Aluguel                   R$  1500.00
  [-] Supermercado              R$   450.00
  [+] Freelance                 R$   800.00
  [-] Conta de luz              R$   180.00
  [-] Internet                  R$    99.90
──────────────────────────────────────
  Total créditos : R$  5800.00
  Total débitos  : R$  2229.90
  Saldo atual    : R$  4570.10
══════════════════════════════════════

Caminhos e crate::

Dentro de módulos, você pode referenciar outros itens do mesmo crate usando caminhos absolutos começando com crate:::

// Caminho absoluto — sempre funciona
use crate::transacao::Transacao;

// Caminho relativo — funciona a partir do módulo atual
use super::transacao::Transacao; // volta um nível
use self::utilidades::formatar;  // nível atual

A preferência idiomática é usar caminhos absolutos com crate:: — são mais explícitos e não quebram quando você reorganiza módulos.

O sistema de módulos de Rust é privado por padrão, e isso muda o hábito: em vez de expor tudo e restringir depois, decide-se item a item o que faz parte da interface. Com módulos, visibilidade granular e a separação entre biblioteca e binário, um projeto deixa de ser um arquivo que cresce sem fim. Vale adotar cedo a divisão em que a lógica mora na biblioteca e o binário é uma casca fina — é o que permite escrever testes de integração contra a API pública, que enxergam apenas o que um usuário externo enxergaria.

Exercícios

Exercício 1

Explique a diferença entre mod, use e pub, e por que um item declarado sem pub não é visível de fora do módulo.

Ver resposta

✓ Resposta: mod declara um módulo, criando um espaço de nomes; use traz um caminho para o escopo atual, encurtando a escrita sem alterar visibilidade; pub torna o item acessível de fora do módulo onde foi declarado. O padrão em Rust é privado: o módulo filho enxerga o pai, mas não o contrário. Isso obriga a decidir explicitamente o que faz parte da interface, em vez de expor tudo por omissão.

Exercício 2

Um projeto tem src/main.rs e precisa de um módulo config em arquivo separado. Mostre as duas formas de organizar e o que cada uma exige.

Ver resposta

✓ Resposta: A forma moderna usa src/config.rs, com mod config; declarado em main.rs. Para um módulo com submódulos, cria-se a pasta src/config/ com src/config.rs ao lado dela declarando os filhos. A forma antiga usava src/config/mod.rs — ainda aceita, mas produz vários arquivos chamados mod.rs abertos ao mesmo tempo no editor. Em qualquer uma, o mod config; é obrigatório: é ele que inclui o arquivo na compilação. O arquivo existir na pasta não basta.

Exercício 3

Explique a diferença entre crate::, self:: e super:: num caminho.

Ver resposta

✓ Resposta: crate:: parte da raiz do crate atual, formando um caminho absoluto — estável mesmo que o módulo seja movido de lugar. self:: parte do módulo atual e super:: sobe um nível, ambos relativos. A recomendação prática é usar caminho absoluto com crate:: para itens de outros ramos da árvore e relativo com super:: quando o item está logo acima — assim, mover um módulo inteiro quebra menos caminhos.

Exercício 4

Qual a diferença entre src/main.rs e src/lib.rs num projeto Cargo, e por que muitos projetos têm os dois?

Ver resposta

✓ Resposta: main.rs é o ponto de entrada de um binário executável; lib.rs é a raiz de uma biblioteca, que outros crates podem importar. Projetos têm os dois quando a lógica deve ser reutilizável e testável: ela vive na biblioteca, e o binário fica sendo uma casca fina que lê argumentos e chama a API pública. A vantagem concreta é que testes de integração em tests/ só enxergam a biblioteca — o que estiver apenas no main.rs fica fora do alcance deles.

Exercício 5

Explique o que o Cargo.toml declara e a diferença entre ele e o Cargo.lock.

Ver resposta

✓ Resposta: O Cargo.toml é escrito por quem desenvolve e declara a intenção: nome, versão, edição e as dependências com as faixas de versão aceitas. O Cargo.lock é gerado pelo Cargo e registra as versões exatas que foram resolvidas, incluindo as dependências indiretas. A regra prática: versione o Cargo.lock em binários, para que todos compilem exatamente o mesmo; em bibliotecas ele importa menos, porque quem consome resolve as próprias versões.

Fontes e leituras recomendadas

Comentários

Mais em Rust

Performance e Otimização — Profiling, SIMD e Técnicas Avançadas
Performance e Otimização — Profiling, SIMD e Técnicas Avançadas

Otimizar sem medir troca legibilidade por nada, porque a intuição sobre onde o…

Banco de Dados com SQLx — Persistência Real para sua API
Banco de Dados com SQLx — Persistência Real para sua API

O SQLx não esconde o banco atrás de objetos: o SQL continua SQL, e as macros o…

Tratamento de Erros com Result — Erros como Valores, não Exceções
Tratamento de Erros com Result — Erros como Valores, não Exceções

Sem exceções, o erro em Rust faz parte do tipo de retorno e o compilador cobra…