In our previous lesson, you learned how to eliminate duplicated code through method extraction and the refactoring of magic numbers in Ruby. This lesson builds upon that foundation by introducing another crucial refactoring technique: the Extract Method. This technique is vital for transforming long, complex methods into smaller, more manageable ones, enhancing both readability and maintainability. As we delve into this lesson, remember that our goal is to follow the Red, Green, Refactor workflow, ensuring that we can use our tests when refactoring to confirm that we have not changed anything regarding behavior. If you change behavior, it is not a successful refactor.
Long methods are a code smell that can hinder efficient development as they often become difficult to understand, test, and maintain. A method might be considered long if it handles multiple responsibilities, making the code harder to track and debug. This complexity can impede our ability to effectively refactor and validate code, as isolated testing of functionalities becomes more challenging. Our task is to identify such cumbersome methods and employ the Extract Method
technique to break them down into smaller, focused sub-methods, each with a single responsibility.
Take a look at the following method. Notice how it is not only long, but it is also responsible for doing a lot of things. Can you identify the different things this method is responsible for defining?
Ruby1def process_user_registration(user_data) 2 begin 3 # Input validation 4 if user_data[:username].nil? || user_data[:username].length < 3 || user_data[:username].length > 20 5 return { success: false, message: "Invalid username. Must be between 3 and 20 characters." } 6 end 7 8 if user_data[:email].nil? || !user_data[:email].include?('@') || !user_data[:email].include?('.') 9 return { success: false, message: "Invalid email format." } 10 end 11 12 if user_data[:password].nil? || user_data[:password].length < 8 || 13 !(user_data[:password] =~ /[A-Z]/) || 14 !(user_data[:password] =~ /[0-9]/) || 15 !(user_data[:password] =~ /[!@#$%^&*]/) 16 return { 17 success: false, 18 message: "Password must be at least 8 characters and contain uppercase, number, and special character." 19 } 20 end 21 22 # Date validation 23 birth_date = Date.parse(user_data[:date_of_birth]) rescue nil 24 age = ((Date.today - birth_date).to_i / 365.25).to_i if birth_date 25 if birth_date.nil? || age < 18 || age > 120 26 return { success: false, message: "Invalid date of birth or user must be 18+" } 27 end 28 29 # Address validation 30 if user_data[:address][:street].nil? || user_data[:address][:street].length < 5 31 return { success: false, message: "Invalid street address" } 32 end 33 if user_data[:address][:city].nil? || user_data[:address][:city].length < 2 34 return { success: false, message: "Invalid city" } 35 end 36 if user_data[:address][:country].nil? || user_data[:address][:country].length < 2 37 return { success: false, message: "Invalid country" } 38 end 39 if user_data[:address][:postal_code].nil? || !(user_data[:address][:postal_code] =~ /^[A-Z0-9]{3,10}$/) 40 return { success: false, message: "Invalid postal code" } 41 end 42 43 # Data transformation 44 user_id = "USER_#{SecureRandom.alphanumeric(9)}" 45 normalized_data = { 46 username: user_data[:username].downcase, 47 email: user_data[:email].downcase, 48 password: user_data[:password], 49 date_of_birth: birth_date, 50 address: { 51 street: user_data[:address][:street].strip, 52 city: user_data[:address][:city].strip, 53 country: user_data[:address][:country].upcase, 54 postal_code: user_data[:address][:postal_code].upcase 55 } 56 } 57 58 data_store.store(normalized_data) 59 60 { success: true, message: "User registered successfully", user_id: user_id } 61 rescue StandardError => error 62 { success: false, message: "Registration failed: #{error.message}" } 63 end 64end
The process_user_registration
method performs multiple tasks:
- User Validation: Checks that the username, email, and password meet specific criteria.
- Date Validation: Verifies that the user’s date of birth is valid and within a specific age range.
- Address Validation: Ensures that each part of the address (e.g., street, city, country, postal code) follows certain rules.
- Data Transformation: Normalizes data (e.g., converting email and username to lowercase).
- Data Storage: Saves the user data to the datastore.
- Error Handling: Catches and returns any errors encountered.
By isolating each of these responsibilities into separate methods, we can improve readability and reusability, and make our code easier to maintain.
We can extract the user validation functionality into its own method:
Ruby1def validate_user(user_data) 2 # Check username validity 3 if user_data[:username].nil? || user_data[:username].length < 3 || user_data[:username].length > 20 4 return { success: false, message: "Invalid username. Must be between 3 and 20 characters." } 5 end 6 7 # Verify email format validity 8 if user_data[:email].nil? || !user_data[:email].include?('@') || !user_data[:email].include?('.') 9 return { success: false, message: "Invalid email format." } 10 end 11 12 # Enforce password complexity requirements 13 if user_data[:password].nil? || user_data[:password].length < 8 || 14 !(user_data[:password] =~ /[A-Z]/) || 15 !(user_data[:password] =~ /[0-9]/) || 16 !(user_data[:password] =~ /[!@#$%^&*]/) 17 return { 18 success: false, 19 message: "Password must be at least 8 characters and contain uppercase, number, and special character." 20 } 21 end 22 23 # Return nil if all validations pass 24 nil 25end
By applying the Extract Method
, our code benefits by becoming more readable and allowing for individual components to be reusable across our codebase. Testing specific functionalities separately enhances our debugging capabilities and reduces complexity. This approach aligns with the principles of making small, incremental changes followed by comprehensive testing.
In this lesson, we have built on our refactoring skills by employing the Extract Method
pattern to manage long methods within a class. Key takeaways include:
- Identifying long methods and articulating their issues.
- Reinforcing the benefits of maintainable and readable code through method extraction.
As you proceed, the upcoming practice exercises will give you hands-on experience in implementing these techniques. Continue applying these principles in your work for more efficient, robust, and scalable applications. Keep up the excellent work! You're well on your way to mastering clean and sustainable code practices.