Refactoring Long Functions in Rust Using Extract Function Technique

Introduction & Context Setting

In this lesson, we focus on improving code maintainability and readability in Rust by refactoring long functions. A lengthy and complex function can be a major bottleneck in understanding and maintaining your code. By breaking it down into smaller, more focused functions, we can enhance its maintainability. We'll follow the Test Driven Development (TDD) approach: Red, Green, Refactor. This iterative process will help ensure that our refactoring does not alter the existing functionality, with tests guiding each step.

Understanding the Problem with Long Functions

Long functions can make code difficult to understand, test, and maintain. When a function handles multiple responsibilities, tracking and debugging become overwhelming, undermining the TDD cycle's efficiency. The task here is to identify lengthy functions and break them apart using the Extract Function technique, ensuring each smaller function is focused and centered on a single task.

An Example of a Long Function

Consider the following Rust function. It is long and manages various tasks. Let's identify the responsibilities this function handles.

struct UserData {
    username: String,
    email: String,
    password: String,
    date_of_birth: String,
    address: Address,
}

struct Address {
    street: String,
    city: String,
    country: String,
    postal_code: String,
}

struct RegistrationResponse {
    success: bool,
    message: String,
    user_id: Option<String>,
}

impl UserData {
    fn process_user_registration(&self) -> RegistrationResponse {
        // User validation
        if self.username.len() < 3 || self.username.len() > 20 {
            return RegistrationResponse {
                success: false,
                message: "Invalid username. Must be between 3 and 20 characters.".to_string(),
                user_id: None,
            };
        }
        
        if !self.email.contains('@') || !self.email.contains('.') {
            return RegistrationResponse {
                success: false,
                message: "Invalid email format.".to_string(),
                user_id: None,
            };
        }
        
        if self.password.len() < 8 
            || !self.password.chars().any(|c| c.is_ascii_uppercase())
            || !self.password.chars().any(|c| c.is_digit(10))
            || !self.password.chars().any(|c| ['!', '@', '#', '$', '%', '^', '&', '*'].contains(&c)) {
            return RegistrationResponse {
                success: false,
                message: "Password must be at least 8 characters and contain uppercase, number, and special character.".to_string(),
                user_id: None,
            };
        }

        // Date validation
        let birth_date = match chrono::NaiveDate::parse_from_str(&self.date_of_birth, "%Y-%m-%d") {
            Ok(date) => date,
            Err(_) => {
                return RegistrationResponse {
                    success: false,
                    message: "Invalid date of birth format.".to_string(),
                    user_id: None,
                };
            }
        };

        let today = chrono::Local::now().date_naive();
        let age_years = today.signed_duration_since(birth_date).num_days() / 365;
        
        if age_years < 18 || age_years > 120 {
            return RegistrationResponse {
                success: false,
                message: "User must be between 18 and 120 years old.".to_string(),
                user_id: None,
            };
        }

        // Address validation
        let address = &self.address;
        if address.street.trim().is_empty() || address.street.len() < 5 {
            return RegistrationResponse {
                success: false,
                message: "Invalid street address".to_string(),
                user_id: None,
            };
        }
        
        if address.city.trim().is_empty() || address.city.len() < 2 {
            return RegistrationResponse {
                success: false,
                message: "Invalid city".to_string(),
                user_id: None,
            };
        }

        if address.country.trim().is_empty() || address.country.len() < 2 {
            return RegistrationResponse {
                success: false,
                message: "Invalid country".to_string(),
                user_id: None,
            };
        }

        let postal_code_regex = regex::Regex::new(r"^[A-Z0-9]{3,10}$").unwrap();
        if address.postal_code.trim().is_empty() || 
           !postal_code_regex.is_match(&address.postal_code) {
            return RegistrationResponse {
                success: false,
                message: "Invalid postal code".to_string(),
                user_id: None,
            };
        }

        // Transform and store data
        let user_id = format!("USER_{:x}", chrono::Local::now().timestamp_nanos_opt().unwrap());
        let normalized_data = UserData {
            username: self.username.to_lowercase(),
            email: self.email.to_lowercase(),
            password: self.password.clone(),
            date_of_birth: birth_date.format("%Y-%m-%d").to_string(),
            address: Address {
                street: address.street.trim().to_string(),
                city: address.city.trim().to_string(),
                country: address.country.trim().to_uppercase(),
                postal_code: address.postal_code.trim().to_uppercase(),
            },
        };
        // Assume store_data is a method that saves the data to a datastore
        // self.store_data(normalized_data);
        RegistrationResponse {
            success: true,
            message: "User registered successfully".to_string(),
            user_id: Some(user_id),
        }
    }
}

The process_user_registration function handles:

  • User Validation: Ensuring the username, email, and password meet specific criteria.
  • Date Validation: Validating the date of birth and ensuring the user is of a valid age.
  • Address Validation: Verifying address fields meet the specified standards.
  • Data Transformation: Normalizing user data.
  • Data Storage: Simulated saving of data (in comments).
  • Error Handling: Managing unexpected errors.

By moving each responsibility into its dedicated function, we can significantly improve readability and reusability.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal