Curso gratuito de Rust: A linguagem mais amada
Refatoração e Boas Práticas no Projeto Final
Aprenda sobre refatoração e boas práticas no projeto final
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
mainse 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 quesn.get_user_guessé melhor queget_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 commentsque usamos nas funçõesgenerate_secret_number,get_user_guess,compare_guess). matchvs.if let: Usematchquando você precisa lidar com todos os casos de um enum (comoOrderingouResult). Useif letquando 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 // }usestatements: 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 fmtSe você quiser ver as diferenças sem aplicar as mudanças:
cargo fmt --checkclippy: 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 clippyclippy pode sugerir coisas como:
- Usar
if letem vez dematchcom um únicoOkouErre 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 testEste 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 umu32aleatório. - Crie uma função
get_user_guess()que solicite a entrada do usuário, trate erros de parse e retorne umu32válido. - Crie uma função
compare_guess(guess: u32, secret_number: u32)que compare os números e retornetruese o palpite estiver correto. - Reestruture a função
main()para usar essas novas funções, tornando-a mais limpa e focada na orquestração.
- Crie uma funçã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 deprintln!.
- Garanta que
-
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.
- Adicione
-
4. Usar
rustfmt:- Execute
cargo fmtno seu projeto para garantir que todo o seu código esteja formatado consistentemente.
- Execute
-
5. Usar
clippy:- Execute
cargo clippye revise as sugestões. Tente entender o porquê de cada sugestão e aplique as que fizerem sentido para melhorar seu código.
- Execute
-
6. Escrever Testes:
- Adicione um módulo
#[cfg(test)] mod testsao seumain.rs. - Escreva pelo menos três testes unitários para a sua função
compare_guess(), cobrindo os casosLess,GreatereEqual. - (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 testpara verificar se todos os seus testes passam.
- Adicione um módulo
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
matcheResult. - Escrever código mais legível e idiomático.
- Utilizar ferramentas essenciais como
rustfmteclippypara 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 usandocargo clippyem 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
structseenumspara 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. 💪