projeto

Refatoração e Boas Práticas no Projeto Final

Aprenda sobre refatoração e boas práticas no projeto final

30 min
Aula 5 de 5

Refatoração e Boas Práticas no Projeto Final

Boas-vindas à nossa aula final do módulo de projeto! 👋 Até agora, você construiu um jogo de adivinhação funcional. Mas ser um bom desenvolvedor não é apenas fazer o código funcionar; é fazer o código funcionar bem, ser fácil de entender, manter e escalar.

Nesta aula, vamos mergulhar na refatoração e nas boas práticas de Rust. Vamos transformar seu jogo funcional em um exemplo de código limpo, robusto e idiomático, seguindo as recomendações da documentação oficial e da comunidade Rust. Prepare-se para elevar seu código a um novo nível! 🚀

1. Introdução

Você já tem um jogo de adivinhação que roda. Fantástico! 🎉 No entanto, se olharmos para o main.rs do nosso projeto inicial, ele pode estar um pouco "monolítico", com toda a lógica dentro da função main.

A refatoração é o processo de reestruturar o código existente sem alterar seu comportamento externo. O objetivo é melhorar a legibilidade, a manutenibilidade e a robustez. As boas práticas são diretrizes e convenções que nos ajudam a escrever código de alta qualidade.

Por que refatorar e aplicar boas práticas?

  • Manutenibilidade: Código limpo é mais fácil de entender e corrigir bugs.
  • Escalabilidade: Um código bem estruturado é mais fácil de estender com novas funcionalidades.
  • Colaboração: Outros desenvolvedores (ou seu "eu" futuro) agradecerão por um código claro.
  • Robustez: Tratamento adequado de erros torna seu programa mais resiliente.
  • Idiomaticidade: Escrever código "à maneira Rust" aproveita os pontos fortes da linguagem.

Vamos aplicar esses princípios ao nosso jogo de adivinhação!

2. Explicação Detalhada com Exemplos

Vamos revisar e melhorar diferentes aspectos do nosso jogo.

2.1. Organização do Código: Módulos e Funções

Inicialmente, todo o nosso jogo pode estar dentro da função main. Isso é aceitável para programas muito pequenos, mas para qualquer coisa um pouco mais complexa, é melhor dividir a lógica em funções menores e mais focadas.

Exemplo: Antes (monolítico)

// main.rs (versão simplificada e inicial)
use std::io;
use rand::Rng;
use std::cmp::Ordering;
 
fn main() {
    println!("Adivinhe o número!");
 
    let secret_number = rand::thread_rng().gen_range(1..=100);
 
    loop {
        println!("Por favor, digite seu palpite.");
 
        let mut guess = String::new();
 
        io::stdin()
            .read_line(&mut guess)
            .expect("Falha ao ler a linha");
 
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Por favor, digite um número!");
                continue;
            }
        };
 
        println!("Você palpitou: {guess}");
 
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Muito pequeno!"),
            Ordering::Greater => println!("Muito grande!"),
            Ordering::Equal => {
                println!("Você acertou! 🎉");
                break;
            }
        }
    }
}

Exemplo: Depois (com funções auxiliares)

Podemos extrair a lógica de obtenção do palpite do usuário e a lógica de comparação em funções separadas.

// main.rs (com funções auxiliares)
use std::io;
use rand::Rng;
use std::cmp::Ordering;
 
/// Gera um número secreto aleatório entre 1 e 100.
fn generate_secret_number() -> u32 {
    rand::thread_rng().gen_range(1..=100)
}
 
/// Solicita ao usuário um palpite e o retorna como u32.
/// Continua pedindo até que um número válido seja fornecido.
fn get_user_guess() -> u32 {
    loop {
        println!("Por favor, digite seu palpite.");
 
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Falha ao ler a linha"); // Tratamento de erro básico para read_line
 
        match guess.trim().parse() {
            Ok(num) => return num,
            Err(_) => {
                println!("❌ Entrada inválida! Por favor, digite um número.");
                continue;
            }
        }
    }
}
 
/// Compara o palpite do usuário com o número secreto e imprime o resultado.
/// Retorna true se o palpite estiver correto, false caso contrário.
fn compare_guess(guess: u32, secret_number: u32) -> bool {
    match guess.cmp(&secret_number) {
        Ordering::Less => {
            println!("Muito pequeno! 📉");
            false
        }
        Ordering::Greater => {
            println!("Muito grande! 📈");
            false
        }
        Ordering::Equal => {
            println!("Você acertou! 🎉");
            true
        }
    }
}
 
fn main() {
    println!("Adivinhe o número!");
    let secret_number = generate_secret_number();
 
    loop {
        let guess = get_user_guess();
        println!("Você palpitou: {guess}");
 
        if compare_guess(guess, secret_number) {
            break;
        }
    }
}

👉 Benefícios:

  • Cada função tem uma responsabilidade única (Princípio da Responsabilidade Única).
  • O main se torna mais legível, atuando como um orquestrador.
  • É mais fácil testar funções individuais.

2.2. Tratamento de Erros Robusto

No Rust, o tratamento de erros é explícito e poderoso, geralmente usando o enum Result<T, E>. Usar .expect() ou .unwrap() é conveniente, mas pode fazer seu programa "panic" (travar) se um erro ocorrer. Para um código robusto, devemos tratar os erros de forma mais graciosa.

No nosso jogo de adivinhação, o principal ponto de falha é a entrada do usuário (read_line e parse).

Melhorando o tratamento de parse: Vimos no exemplo acima que substituímos parse().expect("...") por um match que lida com Ok e Err. Isso evita que o programa trave se o usuário digitar algo que não seja um número.

// Dentro de get_user_guess
match guess.trim().parse() {
    Ok(num) => return num,
    Err(e) => { // Podemos até inspecionar o erro 'e' se quisermos
        println!("❌ Entrada inválida! Por favor, digite um número. Erro: {e}");
        continue;
    }
}

Tratamento de read_line: Para um jogo simples, read_line().expect("Falha ao ler a linha") pode ser aceitável, pois uma falha de I/O é geralmente um problema ambiental grave. No entanto, em aplicações mais críticas, você também trataria esse Result explicitamente.

2.3. Legibilidade e Idiomaticidade

  • Nomes de variáveis e funções: Use nomes claros e descritivos. secret_number é melhor que sn. get_user_guess é melhor que get_input.
  • Comentários: Adicione comentários onde a lógica não for imediatamente óbvia, ou para explicar o propósito de funções e módulos (como as doc comments que usamos nas funções generate_secret_number, get_user_guess, compare_guess).
  • match vs. if let: Use match quando você precisa lidar com todos os casos de um enum (como Ordering ou Result). Use if let quando você só se importa com um caso específico e quer ignorar os outros.
    // Exemplo de if let (se você só se importasse com o caso Ok)
    // if let Ok(num) = guess.trim().parse() {
    //     // faz algo com num
    // } else {
    //     // trata o erro
    // }
  • use statements: Mantenha-os no topo do arquivo, organizados.

2.4. Ferramentas de Qualidade de Código

Rust vem com ferramentas poderosas para garantir a qualidade e o estilo do seu código.

rustfmt: Formatação Automática

rustfmt é uma ferramenta que formata seu código Rust de acordo com um estilo padrão da comunidade. Isso garante consistência em projetos e equipes.

Para formatar seu projeto:

cargo fmt

Se você quiser ver as diferenças sem aplicar as mudanças:

cargo fmt --check

clippy: O Linter do Rust

clippy é uma coleção de lints (verificadores de estilo e erros comuns) para Rust. Ele pode encontrar bugs, melhorar a performance e apontar código não idiomático. É como um "professor" que te dá dicas para escrever um Rust melhor.

Para rodar o clippy no seu projeto:

cargo clippy

clippy pode sugerir coisas como:

  • Usar if let em vez de match com um único Ok ou Err e um _.
  • Simplificar expressões.
  • Evitar clones desnecessários.

Sempre preste atenção às sugestões do clippy! Elas são valiosas.

2.5. Testes (Introdução)

Testes são cruciais para garantir que seu código funcione como esperado e continue funcionando após as alterações. Rust tem suporte a testes embutido.

Para o nosso jogo de adivinhação, podemos testar funções auxiliares, como generate_secret_number (embora números aleatórios sejam um pouco mais complexos de testar deterministicamente) ou uma função de validação de entrada, se tivéssemos uma.

Vamos testar uma função simples que verifica se um número está dentro de um certo range:

// Adicione esta função e o bloco de teste em main.rs (ou em lib.rs se você tiver um)
fn is_in_range(num: u32, min: u32, max: u32) -> bool {
    num >= min && num <= max
}
 
#[cfg(test)] // Esta anotação indica que o módulo 'tests' só é compilado durante os testes
mod tests {
    use super::*; // Importa tudo do módulo pai (main.rs)
 
    #[test] // Esta anotação marca a função como um teste
    fn test_is_in_range_valid() {
        assert!(is_in_range(50, 1, 100)); // Esperamos que 50 esteja entre 1 e 100
    }
 
    #[test]
    fn test_is_in_range_too_low() {
        assert!(!is_in_range(0, 1, 100)); // Esperamos que 0 NÃO esteja entre 1 e 100
    }
 
    #[test]
    fn test_is_in_range_too_high() {
        assert!(!is_in_range(101, 1, 100)); // Esperamos que 101 NÃO esteja entre 1 e 100
    }
 
    // Podemos até testar a função de geração de número secreto,
    // mas com um loop para verificar o range, já que é aleatório.
    #[test]
    fn test_generate_secret_number_range() {
        for _ in 0..1000 { // Gera 1000 números e verifica cada um
            let num = generate_secret_number();
            assert!(num >= 1 && num <= 100, "Número secreto fora do range: {}", num);
        }
    }
}

Para rodar os testes:

cargo test

Este comando compilará e executará todos os testes definidos no seu projeto.

3. Código de Exemplo Oficial (Adaptado)

A documentação oficial do Rust (The Rust Programming Language Book) é a melhor fonte de boas práticas. O capítulo 2, "Programming a Guessing Game", já introduz muitos desses conceitos. O código que apresentamos na seção 2.1 como "Depois" é uma adaptação direta das boas práticas ensinadas no livro, focando em modularidade e tratamento de erros com match.

// Exemplo completo do jogo de adivinhação refatorado,
// seguindo as diretrizes de organização e tratamento de erros do Rust Book.
// main.rs
use std::io; // Para entrada/saída
use rand::Rng; // Para geração de números aleatórios
use std::cmp::Ordering; // Para comparação de números
 
/// Gera um número secreto aleatório entre 1 e 100 (inclusive).
///
/// # Exemplos
///
/// ```
/// let secret = generate_secret_number();
/// assert!(secret >= 1 && secret <= 100);
/// ```
fn generate_secret_number() -> u32 {
    rand::thread_rng().gen_range(1..=100)
}
 
/// Solicita ao usuário um palpite e o retorna como um `u32`.
///
/// Continua pedindo ao usuário até que uma entrada numérica válida seja fornecida.
/// Trata erros de leitura de linha e de parse de forma graciosa.
///
/// # Retorna
///
/// Um `u32` que representa o palpite válido do usuário.
fn get_user_guess() -> u32 {
    loop {
        println!("Por favor, digite seu palpite:");
 
        let mut guess = String::new();
 
        // Tratamento de erro para io::stdin().read_line()
        io::stdin()
            .read_line(&mut guess)
            .expect("Falha ao ler a linha de entrada"); // Em um app mais robusto, você trataria este Result explicitamente
 
        // Tratamento de erro para parse() usando match
        match guess.trim().parse() {
            Ok(num) => return num, // Se o parse for bem-sucedido, retorna o número
            Err(e) => {
                // Se houver um erro de parse, imprime uma mensagem e continua o loop
                eprintln!("❌ Entrada inválida! Por favor, digite um número. Detalhes: {e}");
                continue;
            }
        }
    }
}
 
/// Compara o palpite do usuário com o número secreto.
///
/// Imprime mensagens indicando se o palpite é muito pequeno, muito grande ou correto.
///
/// # Argumentos
///
/// * `guess` - O palpite do usuário (u32).
/// * `secret_number` - O número secreto a ser adivinhado (u32).
///
/// # Retorna
///
/// `true` se o palpite for igual ao número secreto, `false` caso contrário.
fn compare_guess(guess: u32, secret_number: u32) -> bool {
    match guess.cmp(&secret_number) {
        Ordering::Less => {
            println!("Muito pequeno! 📉");
            false
        }
        Ordering::Greater => {
            println!("Muito grande! 📈");
            false
        }
        Ordering::Equal => {
            println!("Você acertou! 🎉");
            true
        }
    }
}
 
fn main() {
    println!("Adivinhe o número!");
    println!("O número secreto será entre 1 e 100.");
 
    let secret_number = generate_secret_number();
 
    loop {
        let guess = get_user_guess();
        println!("Você palpitou: {guess}");
 
        if compare_guess(guess, secret_number) {
            break; // Sai do loop se o palpite estiver correto
        }
    }
 
    println!("Obrigado por jogar!");
}
 
// --- Bloco de Testes ---
#[cfg(test)]
mod tests {
    use super::*; // Importa tudo do módulo pai (main.rs)
 
    // Teste para garantir que o número secreto está no range esperado
    #[test]
    fn test_generate_secret_number_range() {
        for _ in 0..1000 { // Testa 1000 vezes para cobrir a aleatoriedade
            let num = generate_secret_number();
            assert!(num >= 1 && num <= 100, "Número secreto fora do range: {}", num);
        }
    }
 
    // Testes para a função de comparação
    #[test]
    fn test_compare_guess_less() {
        assert!(!compare_guess(10, 20)); // 10 é menor que 20
    }
 
    #[test]
    fn test_compare_guess_greater() {
        assert!(!compare_guess(30, 20)); // 30 é maior que 20
    }
 
    #[test]
    fn test_compare_guess_equal() {
        assert!(compare_guess(20, 20)); // 20 é igual a 20
    }
}

4. Exercícios/Desafios

Agora é a sua vez de aplicar essas boas práticas ao seu próprio projeto de jogo de adivinhação!

Tarefas de Refatoração 🛠️

Complete as seguintes tarefas no seu projeto:

  • 1. Organizar em Funções:

    • Crie uma função generate_secret_number() que retorne um u32 aleatório.
    • Crie uma função get_user_guess() que solicite a entrada do usuário, trate erros de parse e retorne um u32 válido.
    • Crie uma função compare_guess(guess: u32, secret_number: u32) que compare os números e retorne true se o palpite estiver correto.
    • Reestruture a função main() para usar essas novas funções, tornando-a mais limpa e focada na orquestração.
  • 2. Tratamento de Erros Robusto:

    • Garanta que get_user_guess() lide com entradas não-numéricas de forma graciosa, solicitando a entrada novamente até que um número válido seja fornecido.
    • Use eprintln! para mensagens de erro, que escreve na saída de erro padrão, em vez de println!.
  • 3. Adicionar Comentários de Documentação:

    • Adicione doc comments (com ///) para cada função que você criou, explicando seu propósito, argumentos e o que ela retorna. Siga o estilo do exemplo oficial.
  • 4. Usar rustfmt:

    • Execute cargo fmt no seu projeto para garantir que todo o seu código esteja formatado consistentemente.
  • 5. Usar clippy:

    • Execute cargo clippy e revise as sugestões. Tente entender o porquê de cada sugestão e aplique as que fizerem sentido para melhorar seu código.
  • 6. Escrever Testes:

    • Adicione um módulo #[cfg(test)] mod tests ao seu main.rs.
    • Escreva pelo menos três testes unitários para a sua função compare_guess(), cobrindo os casos Less, Greater e Equal.
    • (Opcional, mas recomendado) Adicione um teste para a função generate_secret_number() para verificar se os números gerados estão dentro do range esperado (use um loop como no exemplo).
    • Execute cargo test para verificar se todos os seus testes passam.

5. Resumo e Próximos Passos

Parabéns! 🎉 Você não apenas construiu um jogo de adivinhação, mas também o refatorou para ser um exemplo de código Rust de alta qualidade. Você aprendeu a:

  • Organizar seu código usando funções para melhor modularidade.
  • Tratar erros de forma robusta com match e Result.
  • Escrever código mais legível e idiomático.
  • Utilizar ferramentas essenciais como rustfmt e clippy para garantir a qualidade do código.
  • Introduzir testes unitários para verificar o comportamento do seu programa.

Essas habilidades são fundamentais para qualquer projeto Rust e o colocarão à frente na sua jornada de programação.

Próximos Passos:

  • Explore mais clippy: Continue usando cargo clippy em todos os seus projetos futuros. Ele é um mentor valioso.
  • Mais sobre Testes: O capítulo de testes no The Rust Programming Language Book é excelente e cobre tópicos mais avançados.
  • Estruturas e Enums: À medida que seus projetos crescem, você começará a usar structs e enums para organizar dados e comportamentos complexos.
  • Compartilhe seu projeto: Não hesite em mostrar seu jogo de adivinhação refatorado para a comunidade!

Você está no caminho certo para se tornar um desenvolvedor Rust proficiente! Continue praticando e explorando. 💪

© 2025 Escola All Dev. Todos os direitos reservados.

Refatoração e Boas Práticas no Projeto Final - Curso gratuito de Rust: A linguagem mais amada | escola.all.dev.br