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.