Rust

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

11 min de leitura

Módulos e Organização de Código — Estruturando Projetos Reais
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 r

 

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 que vem a seguir

Com módulos dominados, você tem as ferramentas para organizar qualquer projeto de tamanho real. Nos próximos artigos vamos começar a explorar territórios mais avançados: testes automatizados, concorrência, e as crates mais importantes do ecossistema Rust.

O arco dos primeiros 15 artigos cobre o núcleo da linguagem — ownership, tipos, traits, generics, lifetimes, e organização. Daqui em diante vamos aplicar esses fundamentos em contextos cada vez mais ricos e realistas.


Fontes e leituras recomendadas

Comentários

Mais em Rust

Macros — Escrevendo Código que Escreve Código
Macros — Escrevendo Código que Escreve Código

Desde o primeiro artigo usamos macros sem entender completamente o que são: p...

Generics — Código que Funciona para Qualquer Tipo
Generics — Código que Funciona para Qualquer Tipo

&nbsp; No artigo anterior aprendemos que traits definem&nbsp;o que um tipo p...

Async/Await — Concorrência sem Threads com Tokio
Async/Await — Concorrência sem Threads com Tokio

No artigo anterior exploramos concorrência com threads do sistema operacional...