Long Method Refactoring: Extract Method Technique

Introduction & Context Setting

In our previous lesson, you learned how to eliminate duplicated code through function extraction and the refactoring of magic numbers. This lesson builds upon that foundation by introducing another crucial refactoring technique: the Extract Method. This technique is vital for transforming long, complex functions into smaller, more manageable ones, enhancing both readability and maintainability. As we delve into this lesson, remember that our goal is to follow the Test-Driven Development (TDD) workflow: Red, Green, Refactor. This iterative cycle ensures that we can leverage our tests when refactoring to confirm that we have not changed anything about the behavior. If you change behavior, it is not a successful refactor.

Understanding the Problem with Long Methods

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 employ the TDD cycle, 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.

An example of a Long Method

Take a look at the following function. Notice how it is not only long, but it is also responsible for doing a lot of things. Can you identify the different tasks this function is handling?

Python
def process_user_registration(self, user_data):
    try:
        if not user_data['username'] or len(user_data['username']) < 3 or \
                len(user_data['username']) > 20:
            return {
                'success': False,
                'message': "Invalid username. "
                            "Must be between 3 and 20 characters.",
            }

        if not user_data['email'] or '@' not in user_data['email'] or \
                '.' not in user_data['email']:
            return {'success': False, 'message': "Invalid email format."}

        if not user_data['password'] or len(user_data['password']) < 8 or \
                not any(c.isupper() for c in user_data['password']) or \
                not any(c.isdigit() for c in user_data['password']) or \
                not any(c in '!@#$%^&*' for c in user_data['password']):
            return {
                'success': False,
                'message': "Password must be at least 8 characters and "
                            "contain uppercase, number, and special "
                            "character.",
            }

        birth_date = datetime.strptime(
            user_data['date_of_birth'], "%Y-%m-%d")
        today = datetime.now()
        age = today.year - birth_date.year
        if age < 18 or age > 120:
            return {
                'success': False,
                'message': "Invalid date of birth or user must be 18+",
            }

        if not user_data['address']['street'] or \
                len(user_data['address']['street']) < 5:
            return {'success': False, 'message': "Invalid street address"}
            
        if not user_data['address']['city'] or \
                len(user_data['address']['city']) < 2:
            return {'success': False, 'message': "Invalid city"}
            
        if not user_data['address']['country'] or \
                len(user_data['address']['country']) < 2:
            return {'success': False, 'message': "Invalid country"}
            
        if not user_data['address']['postal_code'] or \
                not user_data['address']['postal_code'].isalnum() or \
                len(user_data['address']['postal_code']) < 3:
            return {'success': False, 'message': "Invalid postal code"}

        chars = random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=9)
        user_id = f"USER_{''.join(chars)}"
        
        normalized_data = {
            'username': user_data['username'].lower(),
            'email': user_data['email'].lower(),
            'password': user_data['password'],
            'date_of_birth': birth_date.isoformat(),
            'address': {
                'street': user_data['address']['street'].strip(),
                'city': user_data['address']['city'].strip(),
                'country': user_data['address']['country'].upper(),
                'postal_code': user_data['address']['postal_code'].upper(),
            }
        }

        self.data_store.store(normalized_data)
        
        return {
            'success': True,
            'message': "User registered successfully",
            'user_id': user_id,
        }
    except Exception as e:
        return {
            'success': False,
            'message': f"Registration failed: {str(e)}",
        }

The process_user_registration function 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.

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