Refactoring Long Methods with the Extract Method Technique in Go

Introduction & Context Setting

In our previous lesson, you learned how to eliminate duplicated code in Go by extracting functions and refactoring magic numbers. This lesson builds on that foundation by identifying another prevalent code smell — long functions. Transforming lengthy, complex functions into smaller, more manageable ones enhances readability and maintainability. Throughout this lesson, we'll adhere to the Test Driven Development (TDD) workflow: Red, Green, Refactor. Using this iterative cycle ensures that our refactoring maintains the existing behavior, with tests verifying that any adjustments have not altered functionality.

Understanding the Problem with Long Functions

Long functions can be a significant obstacle in software development, making code difficult to understand, test, and maintain. When a function tackles multiple responsibilities, it becomes harder to track and debug, complicating the TDD cycle's effectiveness. Our task is to identify these lengthy functions and use the Extract Function technique to break them down into smaller, focused functions, each with a single purpose.

An Example of a Long Function

Consider the following Go function. It is not only lengthy but also handles various tasks. Can you identify what responsibilities this function includes?

func (urs *UserRegistrationService) ProcessUserRegistration(userData UserData) RegistrationResponse {
    // TODO: Extract User Validation logic, not including the date logic
    // User validation
    if len(userData.Username) < 3 || len(userData.Username) > 20 {
        return RegistrationResponse{false, "Invalid username. Must be between 3 and 20 characters.", ""}
    }
    
    if !strings.Contains(userData.Email, "@") || !strings.Contains(userData.Email, ".") {
        return RegistrationResponse{false, "Invalid email format.", ""}
    }
    
    hasUpper, _ := regexp.MatchString(`[A-Z]`, userData.Password)
    hasNumber, _ := regexp.MatchString(`[0-9]`, userData.Password)
    hasSpecial, _ := regexp.MatchString(`[!@#$%^&*]`, userData.Password)

    if len(userData.Password) < 8 || !hasUpper || !hasNumber || !hasSpecial {
        return RegistrationResponse{false, "Password must be at least 8 characters and contain uppercase, number, and special character.", ""}
    }
    
    // Date validation
    birthDate, err := time.Parse("2006-01-02", userData.DateOfBirth)
    if err != nil {
        return RegistrationResponse{false, err.Error(), ""}
    }

    if birthDate.AddDate(18, 0, 0).After(time.Now()) || birthDate.AddDate(120, 0, 0).Before(time.Now()) {
        return RegistrationResponse{false, "Invalid date of birth or user must be 18+", ""}
    }
    
    // Address validation
    address := userData.Address
    // Street validation
    if strings.TrimSpace(address.Street) == "" || len(address.Street) < 5 {
        return RegistrationResponse{false, "Invalid street address", ""}
    }

    // City validation
    if strings.TrimSpace(address.City) == "" || len(address.City) < 2 {
        return RegistrationResponse{false, "Invalid city", ""}
    }

    // Country validation
    if strings.TrimSpace(address.Country) == "" || len(address.Country) < 2 {
        return RegistrationResponse{false, "Invalid country", ""}
    }

    // Postal code validation
    var postalCodeRegex = regexp.MustCompile(`^[A-Z0-9]{3,10}$`)
    if strings.TrimSpace(address.PostalCode) == "" || 
       !postalCodeRegex.MatchString(address.PostalCode) {
        return RegistrationResponse{false, "Invalid postal code", ""}
    } 
    
    // Transform and store data
    userId := fmt.Sprintf("USER_%x", time.Now().UnixNano())
    normalizedData := UserData{
        Username:    strings.ToLower(userData.Username),
        Email:       strings.ToLower(userData.Email),
        Password:    userData.Password,
        DateOfBirth: birthDate.Format("2006-01-02"),
        Address: Address{
            Street:    strings.TrimSpace(userData.Address.Street),
            City:      strings.TrimSpace(userData.Address.City),
            Country:   strings.ToUpper(strings.TrimSpace(userData.Address.Country)),
            PostalCode: strings.ToUpper(strings.TrimSpace(userData.Address.PostalCode)),
        },
    }
    urs.dataStore.Store(normalizedData)
    return RegistrationResponse{true, "User registered successfully", userId}
}

The ProcessUserRegistration function encompasses multiple responsibilities:

  • User Validation: Checks that the username, email, and password meet specific criteria.
  • Date Validation: Ensures that the date of birth is valid and the user is of a certain age.
  • Address Validation: Ensures that address fields adhere to specified rules.
  • Data Transformation: Normalizes the user data.
  • Data Storage: Saves the data using the datastore.
  • Error Handling: Manages unexpected errors.

By isolating each responsibility into separate functions, we can significantly enhance code 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