Composite em Rust

O padrao Composite em Rust: enums recursivos, Box para alocacao no heap, hierarquias em arvore e exemplos praticos de sistema de arquivos, UI e AST.

Introducao

O Composite (Composto) e um padrao estrutural que permite tratar objetos individuais e composicoes de objetos de maneira uniforme. Ele organiza objetos em estruturas de arvore para representar hierarquias parte-todo.

Em Rust, o Composite se manifesta naturalmente atraves de enums recursivos com Box para alocacao no heap. Essa abordagem e idiomatica, type-safe e extremamente eficiente. Diferente de linguagens com heranca onde voce precisa de classes abstratas e hierarquias complexas, em Rust um simples enum ja captura toda a expressividade do padrao.


Problema

Voce precisa representar uma estrutura hierarquica onde elementos podem conter outros elementos. Exemplos classicos: sistema de arquivos (arquivos e pastas), interface grafica (botoes dentro de paineis dentro de janelas), ou uma expressao matematica (numeros, operadores e subexpressoes).

// Sem Composite: tipos separados que nao se compoem
struct Arquivo {
    nome: String,
    tamanho: u64,
}

struct Pasta {
    nome: String,
    arquivos: Vec<Arquivo>,
    subpastas: Vec<Pasta>,
}

// Problema: preciso duplicar logica para Arquivo e Pasta
// Problema: nao posso misturar arquivos e pastas em uma lista unica
// Problema: nao posso tratar ambos uniformemente

Solucao em Rust

Enum Recursivo: O Composite Idiomatico

use std::fmt;

/// Um no na arvore do sistema de arquivos.
/// Note como o enum unifica folhas (Arquivo) e compostos (Diretorio).
#[derive(Debug, Clone)]
pub enum EntradaFs {
    /// Folha: um arquivo com nome e tamanho
    Arquivo {
        nome: String,
        tamanho_bytes: u64,
        extensao: Option<String>,
    },
    /// Link simbolico (outra folha)
    Link {
        nome: String,
        destino: String,
    },
    /// Composto: um diretorio que contem outras entradas
    Diretorio {
        nome: String,
        /// Box nao e necessario aqui porque Vec ja aloca no heap
        filhos: Vec<EntradaFs>,
    },
}

impl EntradaFs {
    /// Cria um novo arquivo
    pub fn arquivo(nome: &str, tamanho: u64) -> Self {
        let extensao = nome
            .rsplit_once('.')
            .map(|(_, ext)| ext.to_string());

        EntradaFs::Arquivo {
            nome: nome.to_string(),
            tamanho_bytes: tamanho,
            extensao,
        }
    }

    /// Cria um novo diretorio vazio
    pub fn diretorio(nome: &str) -> Self {
        EntradaFs::Diretorio {
            nome: nome.to_string(),
            filhos: Vec::new(),
        }
    }

    /// Cria um link simbolico
    pub fn link(nome: &str, destino: &str) -> Self {
        EntradaFs::Link {
            nome: nome.to_string(),
            destino: destino.to_string(),
        }
    }

    /// Adiciona uma entrada filha (so funciona em diretorios)
    pub fn adicionar(mut self, filho: EntradaFs) -> Self {
        if let EntradaFs::Diretorio { ref mut filhos, .. } = self {
            filhos.push(filho);
        }
        self
    }

    /// Retorna o nome da entrada
    pub fn nome(&self) -> &str {
        match self {
            EntradaFs::Arquivo { nome, .. } => nome,
            EntradaFs::Diretorio { nome, .. } => nome,
            EntradaFs::Link { nome, .. } => nome,
        }
    }

    /// Calcula o tamanho total recursivamente
    /// Essa e a essencia do Composite: mesma operacao em folhas e compostos
    pub fn tamanho_total(&self) -> u64 {
        match self {
            EntradaFs::Arquivo { tamanho_bytes, .. } => *tamanho_bytes,
            EntradaFs::Link { .. } => 0, // links nao contam no tamanho
            EntradaFs::Diretorio { filhos, .. } => {
                // Soma recursiva de todos os filhos
                filhos.iter().map(|f| f.tamanho_total()).sum()
            }
        }
    }

    /// Conta o numero total de arquivos recursivamente
    pub fn contar_arquivos(&self) -> usize {
        match self {
            EntradaFs::Arquivo { .. } => 1,
            EntradaFs::Link { .. } => 0,
            EntradaFs::Diretorio { filhos, .. } => {
                filhos.iter().map(|f| f.contar_arquivos()).sum()
            }
        }
    }

    /// Busca arquivos por extensao recursivamente
    pub fn buscar_por_extensao(&self, ext: &str) -> Vec<&str> {
        match self {
            EntradaFs::Arquivo { nome, extensao, .. } => {
                if extensao.as_deref() == Some(ext) {
                    vec![nome.as_str()]
                } else {
                    Vec::new()
                }
            }
            EntradaFs::Link { .. } => Vec::new(),
            EntradaFs::Diretorio { filhos, .. } => {
                filhos
                    .iter()
                    .flat_map(|f| f.buscar_por_extensao(ext))
                    .collect()
            }
        }
    }

    /// Exibe a arvore com indentacao
    pub fn exibir_arvore(&self, prefixo: &str, e_ultimo: bool) {
        let conector = if e_ultimo { "└── " } else { "├── " };
        let proximo_prefixo = if e_ultimo { "    " } else { "│   " };

        match self {
            EntradaFs::Arquivo { nome, tamanho_bytes, .. } => {
                println!(
                    "{}{}{} ({})",
                    prefixo,
                    conector,
                    nome,
                    formatar_tamanho(*tamanho_bytes)
                );
            }
            EntradaFs::Link { nome, destino } => {
                println!("{}{}{}  -> {}", prefixo, conector, nome, destino);
            }
            EntradaFs::Diretorio { nome, filhos } => {
                println!("{}{}{}/", prefixo, conector, nome);
                let novo_prefixo = format!("{}{}", prefixo, proximo_prefixo);
                for (i, filho) in filhos.iter().enumerate() {
                    filho.exibir_arvore(&novo_prefixo, i == filhos.len() - 1);
                }
            }
        }
    }
}

/// Formata tamanho em bytes para formato legivel
fn formatar_tamanho(bytes: u64) -> String {
    if bytes < 1024 {
        format!("{} B", bytes)
    } else if bytes < 1024 * 1024 {
        format!("{:.1} KB", bytes as f64 / 1024.0)
    } else if bytes < 1024 * 1024 * 1024 {
        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
    } else {
        format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
    }
}

fn main() {
    // Construindo a arvore de arquivos usando o padrao Composite
    let projeto = EntradaFs::diretorio("meu-projeto")
        .adicionar(EntradaFs::arquivo("Cargo.toml", 450))
        .adicionar(EntradaFs::arquivo("README.md", 2048))
        .adicionar(EntradaFs::arquivo(".gitignore", 64))
        .adicionar(
            EntradaFs::diretorio("src")
                .adicionar(EntradaFs::arquivo("main.rs", 1200))
                .adicionar(EntradaFs::arquivo("lib.rs", 800))
                .adicionar(
                    EntradaFs::diretorio("modelos")
                        .adicionar(EntradaFs::arquivo("mod.rs", 150))
                        .adicionar(EntradaFs::arquivo("usuario.rs", 3500))
                        .adicionar(EntradaFs::arquivo("pedido.rs", 4200)),
                )
                .adicionar(
                    EntradaFs::diretorio("handlers")
                        .adicionar(EntradaFs::arquivo("mod.rs", 100))
                        .adicionar(EntradaFs::arquivo("auth.rs", 2800))
                        .adicionar(EntradaFs::arquivo("api.rs", 5600)),
                ),
        )
        .adicionar(
            EntradaFs::diretorio("tests")
                .adicionar(EntradaFs::arquivo("integration_test.rs", 3000))
                .adicionar(EntradaFs::arquivo("helpers.rs", 800)),
        )
        .adicionar(EntradaFs::link("latest", "target/release/meu-projeto"));

    // Operacoes uniformes em toda a arvore
    println!("=== Arvore do Projeto ===");
    projeto.exibir_arvore("", true);

    println!("\n=== Estatisticas ===");
    println!(
        "Tamanho total: {}",
        formatar_tamanho(projeto.tamanho_total())
    );
    println!("Total de arquivos: {}", projeto.contar_arquivos());

    println!("\n=== Arquivos Rust (.rs) ===");
    let arquivos_rust = projeto.buscar_por_extensao("rs");
    for arquivo in &arquivos_rust {
        println!("  {}", arquivo);
    }
    println!("Total: {} arquivos .rs", arquivos_rust.len());
}

Diagrama

ESTRUTURA DO COMPOSITE:

    enum EntradaFs
    ├── Arquivo { nome, tamanho }          <- Folha
    ├── Link { nome, destino }             <- Folha
    └── Diretorio { nome, filhos: Vec<EntradaFs> }  <- Composto
                                     |
                                     +-- contem EntradaFs (recursivo!)


ARVORE DE EXEMPLO:

    Diretorio("projeto")
    ├── Arquivo("Cargo.toml", 450)
    ├── Diretorio("src")
    │   ├── Arquivo("main.rs", 1200)
    │   └── Diretorio("modelos")
    │       ├── Arquivo("mod.rs", 150)
    │       └── Arquivo("usuario.rs", 3500)
    └── Link("latest" -> "target/...")


OPERACAO UNIFORME (tamanho_total):

    Diretorio.tamanho_total()
        = filho1.tamanho_total() + filho2.tamanho_total() + ...
        = Arquivo.tamanho_total() + Diretorio.tamanho_total() + ...
        = 450                     + (1200 + 150 + 3500)        + ...

    A mesma funcao funciona em qualquer nivel da arvore!

Exemplo do Mundo Real

Uma AST (Arvore Sintatica Abstrata) para um mini-interpretador de expressoes:

use std::collections::HashMap;

/// Expressao: o Composite central da AST
/// Box e necessario porque Expr contem Expr (tipo recursivo)
#[derive(Debug, Clone)]
pub enum Expr {
    /// Folha: numero literal
    Numero(f64),

    /// Folha: variavel referenciada pelo nome
    Variavel(String),

    /// Folha: valor booleano
    Booleano(bool),

    /// Folha: texto literal
    Texto(String),

    /// Composto: operacao binaria (contem duas subexpressoes)
    BinOp {
        esquerda: Box<Expr>,
        operador: Operador,
        direita: Box<Expr>,
    },

    /// Composto: operacao unaria (contem uma subexpressao)
    UnOp {
        operador: OpUnario,
        operando: Box<Expr>,
    },

    /// Composto: chamada de funcao (contem lista de argumentos)
    ChamadaFuncao {
        nome: String,
        argumentos: Vec<Expr>,
    },

    /// Composto: expressao condicional (se-entao-senao)
    SeEntao {
        condicao: Box<Expr>,
        entao: Box<Expr>,
        senao: Option<Box<Expr>>,
    },

    /// Composto: bloco de expressoes (retorna o valor da ultima)
    Bloco(Vec<Expr>),
}

#[derive(Debug, Clone, Copy)]
pub enum Operador {
    Soma,
    Subtracao,
    Multiplicacao,
    Divisao,
    Potencia,
    Igual,
    Maior,
    Menor,
}

#[derive(Debug, Clone, Copy)]
pub enum OpUnario {
    Negativo,
    Nao,
}

/// Resultado da avaliacao de uma expressao
#[derive(Debug, Clone)]
pub enum Valor {
    Numero(f64),
    Booleano(bool),
    Texto(String),
    Nulo,
}

impl std::fmt::Display for Valor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Valor::Numero(n) => write!(f, "{}", n),
            Valor::Booleano(b) => write!(f, "{}", b),
            Valor::Texto(s) => write!(f, "\"{}\"", s),
            Valor::Nulo => write!(f, "nulo"),
        }
    }
}

/// Ambiente de execucao (variaveis e funcoes)
pub struct Ambiente {
    variaveis: HashMap<String, Valor>,
}

impl Ambiente {
    pub fn new() -> Self {
        Self {
            variaveis: HashMap::new(),
        }
    }

    pub fn definir(&mut self, nome: &str, valor: Valor) {
        self.variaveis.insert(nome.to_string(), valor);
    }

    pub fn obter(&self, nome: &str) -> Valor {
        self.variaveis
            .get(nome)
            .cloned()
            .unwrap_or(Valor::Nulo)
    }
}

impl Expr {
    /// Avalia a expressao recursivamente (o coracao do Composite)
    pub fn avaliar(&self, env: &mut Ambiente) -> Valor {
        match self {
            // Folhas: retornam diretamente
            Expr::Numero(n) => Valor::Numero(*n),
            Expr::Booleano(b) => Valor::Booleano(*b),
            Expr::Texto(s) => Valor::Texto(s.clone()),
            Expr::Variavel(nome) => env.obter(nome),

            // Compostos: avaliam subexpressoes recursivamente
            Expr::BinOp { esquerda, operador, direita } => {
                let val_esq = esquerda.avaliar(env);
                let val_dir = direita.avaliar(env);

                match (val_esq, operador, val_dir) {
                    (Valor::Numero(a), Operador::Soma, Valor::Numero(b)) => {
                        Valor::Numero(a + b)
                    }
                    (Valor::Numero(a), Operador::Subtracao, Valor::Numero(b)) => {
                        Valor::Numero(a - b)
                    }
                    (Valor::Numero(a), Operador::Multiplicacao, Valor::Numero(b)) => {
                        Valor::Numero(a * b)
                    }
                    (Valor::Numero(a), Operador::Divisao, Valor::Numero(b)) => {
                        if b == 0.0 {
                            Valor::Nulo // Divisao por zero
                        } else {
                            Valor::Numero(a / b)
                        }
                    }
                    (Valor::Numero(a), Operador::Potencia, Valor::Numero(b)) => {
                        Valor::Numero(a.powf(b))
                    }
                    (Valor::Numero(a), Operador::Maior, Valor::Numero(b)) => {
                        Valor::Booleano(a > b)
                    }
                    (Valor::Numero(a), Operador::Menor, Valor::Numero(b)) => {
                        Valor::Booleano(a < b)
                    }
                    (Valor::Numero(a), Operador::Igual, Valor::Numero(b)) => {
                        Valor::Booleano((a - b).abs() < f64::EPSILON)
                    }
                    (Valor::Texto(a), Operador::Soma, Valor::Texto(b)) => {
                        Valor::Texto(format!("{}{}", a, b))
                    }
                    _ => Valor::Nulo,
                }
            }

            Expr::UnOp { operador, operando } => {
                let val = operando.avaliar(env);
                match (operador, val) {
                    (OpUnario::Negativo, Valor::Numero(n)) => Valor::Numero(-n),
                    (OpUnario::Nao, Valor::Booleano(b)) => Valor::Booleano(!b),
                    _ => Valor::Nulo,
                }
            }

            Expr::ChamadaFuncao { nome, argumentos } => {
                let args: Vec<Valor> = argumentos
                    .iter()
                    .map(|a| a.avaliar(env))
                    .collect();

                // Funcoes embutidas
                match nome.as_str() {
                    "max" => {
                        args.iter()
                            .filter_map(|v| if let Valor::Numero(n) = v { Some(*n) } else { None })
                            .reduce(f64::max)
                            .map(Valor::Numero)
                            .unwrap_or(Valor::Nulo)
                    }
                    "abs" => {
                        if let Some(Valor::Numero(n)) = args.first() {
                            Valor::Numero(n.abs())
                        } else {
                            Valor::Nulo
                        }
                    }
                    "imprimir" => {
                        for arg in &args {
                            print!("{} ", arg);
                        }
                        println!();
                        Valor::Nulo
                    }
                    _ => Valor::Nulo,
                }
            }

            Expr::SeEntao { condicao, entao, senao } => {
                let cond = condicao.avaliar(env);
                match cond {
                    Valor::Booleano(true) => entao.avaliar(env),
                    Valor::Booleano(false) => {
                        senao.as_ref()
                            .map(|e| e.avaliar(env))
                            .unwrap_or(Valor::Nulo)
                    }
                    _ => Valor::Nulo,
                }
            }

            Expr::Bloco(exprs) => {
                let mut resultado = Valor::Nulo;
                for expr in exprs {
                    resultado = expr.avaliar(env);
                }
                resultado
            }
        }
    }

    /// Conta o numero total de nos na AST
    pub fn contar_nos(&self) -> usize {
        match self {
            Expr::Numero(_) | Expr::Booleano(_) | Expr::Texto(_) | Expr::Variavel(_) => 1,
            Expr::BinOp { esquerda, direita, .. } => {
                1 + esquerda.contar_nos() + direita.contar_nos()
            }
            Expr::UnOp { operando, .. } => 1 + operando.contar_nos(),
            Expr::ChamadaFuncao { argumentos, .. } => {
                1 + argumentos.iter().map(|a| a.contar_nos()).sum::<usize>()
            }
            Expr::SeEntao { condicao, entao, senao } => {
                1 + condicao.contar_nos()
                    + entao.contar_nos()
                    + senao.as_ref().map_or(0, |e| e.contar_nos())
            }
            Expr::Bloco(exprs) => {
                1 + exprs.iter().map(|e| e.contar_nos()).sum::<usize>()
            }
        }
    }

    /// Representacao textual da expressao
    pub fn para_texto(&self) -> String {
        match self {
            Expr::Numero(n) => format!("{}", n),
            Expr::Booleano(b) => format!("{}", b),
            Expr::Texto(s) => format!("\"{}\"", s),
            Expr::Variavel(nome) => nome.clone(),
            Expr::BinOp { esquerda, operador, direita } => {
                let op = match operador {
                    Operador::Soma => "+",
                    Operador::Subtracao => "-",
                    Operador::Multiplicacao => "*",
                    Operador::Divisao => "/",
                    Operador::Potencia => "^",
                    Operador::Igual => "==",
                    Operador::Maior => ">",
                    Operador::Menor => "<",
                };
                format!("({} {} {})", esquerda.para_texto(), op, direita.para_texto())
            }
            Expr::UnOp { operador, operando } => {
                let op = match operador {
                    OpUnario::Negativo => "-",
                    OpUnario::Nao => "!",
                };
                format!("({}{})", op, operando.para_texto())
            }
            Expr::ChamadaFuncao { nome, argumentos } => {
                let args: Vec<String> = argumentos.iter().map(|a| a.para_texto()).collect();
                format!("{}({})", nome, args.join(", "))
            }
            Expr::SeEntao { condicao, entao, senao } => {
                let base = format!("se {} entao {}", condicao.para_texto(), entao.para_texto());
                match senao {
                    Some(e) => format!("{} senao {}", base, e.para_texto()),
                    None => base,
                }
            }
            Expr::Bloco(exprs) => {
                let stmts: Vec<String> = exprs.iter().map(|e| e.para_texto()).collect();
                format!("{{ {} }}", stmts.join("; "))
            }
        }
    }
}

fn main() {
    let mut env = Ambiente::new();
    env.definir("x", Valor::Numero(10.0));
    env.definir("y", Valor::Numero(3.0));

    // Expressao: (x + y) * 2 - 1
    let expr = Expr::BinOp {
        esquerda: Box::new(Expr::BinOp {
            esquerda: Box::new(Expr::BinOp {
                esquerda: Box::new(Expr::Variavel("x".to_string())),
                operador: Operador::Soma,
                direita: Box::new(Expr::Variavel("y".to_string())),
            }),
            operador: Operador::Multiplicacao,
            direita: Box::new(Expr::Numero(2.0)),
        }),
        operador: Operador::Subtracao,
        direita: Box::new(Expr::Numero(1.0)),
    };

    println!("Expressao: {}", expr.para_texto());
    println!("Resultado: {}", expr.avaliar(&mut env));
    println!("Nos na AST: {}", expr.contar_nos());

    // Expressao condicional: se x > 5 entao max(x, y) senao abs(-x)
    let expr_cond = Expr::SeEntao {
        condicao: Box::new(Expr::BinOp {
            esquerda: Box::new(Expr::Variavel("x".to_string())),
            operador: Operador::Maior,
            direita: Box::new(Expr::Numero(5.0)),
        }),
        entao: Box::new(Expr::ChamadaFuncao {
            nome: "max".to_string(),
            argumentos: vec![
                Expr::Variavel("x".to_string()),
                Expr::Variavel("y".to_string()),
            ],
        }),
        senao: Some(Box::new(Expr::ChamadaFuncao {
            nome: "abs".to_string(),
            argumentos: vec![Expr::UnOp {
                operador: OpUnario::Negativo,
                operando: Box::new(Expr::Variavel("x".to_string())),
            }],
        })),
    };

    println!("\nCondicional: {}", expr_cond.para_texto());
    println!("Resultado: {}", expr_cond.avaliar(&mut env));
}

Quando Usar

  • Arvores e hierarquias (sistema de arquivos, DOM, AST, menus)
  • Operacoes recursivas que devem funcionar uniformemente em toda a arvore
  • Representacao de expressoes matematicas, logicas ou de consulta
  • UI declarativa com componentes aninhados
  • Estruturas de dados compostas onde parte e todo sao intercambiaveis

Quando NAO Usar

  • Listas planas onde nao ha hierarquia real
  • Quando os tipos folha e composto sao muito diferentes - o padrao forca uniformidade
  • Grafos com ciclos - Composite funciona para arvores, nao grafos gerais
  • Performance critica com arvores profundas - a recursao pode estourar a pilha
// Para grafos com ciclos, use indices em vez de Box:
struct Grafo {
    nos: Vec<No>,
    arestas: Vec<(usize, usize)>, // indices em vez de referencias
}

Variacoes em Rust

1. Composite com trait object (mais flexivel)

pub trait Componente {
    fn renderizar(&self) -> String;
    fn tamanho(&self) -> u32;
}

pub struct Grupo {
    filhos: Vec<Box<dyn Componente>>,
}

impl Componente for Grupo {
    fn renderizar(&self) -> String {
        self.filhos.iter().map(|f| f.renderizar()).collect()
    }
    fn tamanho(&self) -> u32 {
        self.filhos.iter().map(|f| f.tamanho()).sum()
    }
}

2. Composite com Rc para compartilhamento

use std::rc::Rc;
use std::cell::RefCell;

// Quando nos podem ter multiplos pais (DAG, nao arvore pura)
type NoRef = Rc<RefCell<No>>;

struct No {
    valor: String,
    filhos: Vec<NoRef>,
}

3. Composite com arena (performance maxima)

// Arena evita alocacoes individuais no heap
struct Arena {
    nos: Vec<NoArena>,
}

struct NoArena {
    valor: String,
    filhos: Vec<usize>, // indices na arena
}

Padroes Relacionados

  • Decorator - Decorator adiciona camadas lineares; Composite cria arvores
  • Strategy - Folhas do Composite podem usar Strategy para variar comportamento
  • Prototype - Clone pode duplicar subarvores inteiras do Composite
  • Builder - Builder pode ser usado para construir arvores Composite complexas

Conclusao

O Composite em Rust demonstra uma das maiores forcas da linguagem: enums com dados. Enquanto linguagens OOP precisam de hierarquias de classes abstratas, interfaces e heranca para implementar o Composite, em Rust um simples enum com variantes recursivas resolve o problema de forma mais concisa, segura e eficiente. O compilador garante que todos os casos sao tratados via match exaustivo, e Box fornece a indirection necessaria para tipos recursivos com custo minimo. Combinado com metodos recursivos, o Composite em Rust e elegante e poderoso.