Refactoring

Software
Code Quality
Refactoring
Last reviewed

February 6, 2025

Last modified

December 3, 2025

“Always leave the code you’re editing a little better than you found it.”

Robert C. Martin (Uncle Bob)

What is refactoring?

Refactoring is the process of restructuring existing code without changing its external behaviour. It improves maintainability and readability, making future developments smoother and reducing the likelihood of bugs. Key benefits include:

  • Improving readability - Writing code that is easier to understand, benefits both yourself and future developers.
  • Reducing complexity - Simplifying complex structures by breaking down large functions or removing unnecessary dependencies.
  • Optimizing design - Creating a more robust and adaptable codebase for long-term growth.
  • Eliminating redundancies - Removing duplicate or unnecessary code.
  • Ensuring consistency - Following a consistent coding style for a cleaner, more maintanable codebase.

When should you refactor?

CC-BY-4.0 © 2021 Balaban et al.
  1. Rule of three: If you find yourself writing the same or similar code for the third time, it’s time to refactor.
  2. Before adding a feature: Cleaning up existing code makes it easier to integrate a new functionality.
  3. When fixing a bug: Cleaning up surrounding code can help uncover and fix the issue faster.
  4. During code reviews: Refactoring during code reviews can prevent issues from becoming part of the public codebase and streamline the development process.
  5. When you spot a code smell: Addressing code smells early prevents them from evolving into more serious bugs.

How to refactor code effectively?

Refactoring should be done gradually, improving code in small controlled steps without introducing new functionalities. Keep these principles in mind:

Maintain clean code - Aim for clarity, simplicity, and readability.

Always work in small steps - So that you can easily identify whether a change to the code changes its behaviour.

Avoid adding new features - Focus on improving structure, not functionality.

Ensure tests pass - Verify that all existing tests succeed before starting with refactoring. If there are no tests, consider writing some basic tests first to cover the existing functionality.

Test often - So that you can be sure the behaviour remains unchanged.

Commit often - Use a version control system and commit often, so that you can easily revert changes if something goes wrong.

Remember, you can stop at any point - Refactoring can be an endless task if you aim for perfection. Instead aim to leave the code in a better state than you found it.

Farley’s refactoring method

Refactoring can be approached in various ways. Here is a simple four-step method proposed by Dave Farley in his online course “Refactoring legacy code”.1 This method emphasizes safety and gradual improvement.

1. Write approval tests

Create software tests for the code that will be refactored. Approval tests are software tests that check the outputs of a program or part of a program. Approval tests are important in refactoring because we need to know if changes in the code affect its behaviour.

2. Reduce clutter

Remove unnecessary code, such as unused (dead) code, and repeated code. While doing so, be cautious when removing code, but take some chances when reducing clutter. To safely reduce clutter, rely on version control to undo changes, and on approval tests to check that code changes do not affect its behaviour.

3. Reduce cyclomatic complexity

Cyclomatic complexity refers to the number of logical branches or pathways used in the code to implement functionality and behaviour. The overuse of if statements and loops is an indication of code with high levels of cyclomatic complexity.

As an example, consider the following code snippet with high cyclomatic complexity due to multiple branching statements:

def get_discount(customer_type, is_holiday):
    if customer_type == "VIP":
        if is_holiday:
            return 0.3
        else:
            return 0.2
    elif customer_type == "MEMBER":
        if is_holiday:
            return 0.2
        else:
            return 0.1
    else:
        if is_holiday:
            return 0.1
        else:
            return 0.0

Instead, we can refactor this code to reduce its cyclomatic complexity by using a dictionary and conditional logic:

def get_discount(customer_type, is_holiday):
    base_discounts = {"VIP": 0.2, "MEMBER": 0.1}
    discount = base_discounts.get(customer_type, 0.0)
    if is_holiday:
        discount += 0.1
    return discount

As an example, consider the following code snippet with high cyclomatic complexity due to multiple branching statements:

get_discount <- function(customer_type, is_holiday) {
  if (customer_type == "VIP") {
    if (is_holiday) {
      return(0.3)
    } else {
      return(0.2)
    }
  } else if (customer_type == "MEMBER") {
    if (is_holiday) {
      return(0.2)
    } else {
      return(0.1)
    }
  } else {
    if (is_holiday) {
      return(0.1)
    } else {
      return(0.0)
    }
  }
}

Instead, we can refactor this code to reduce its cyclomatic complexity by using a named vector and conditional logic:

get_discount <- function(customer_type, is_holiday) {
  base_discounts <- c(VIP = 0.2, MEMBER = 0.1)
  discount <- base_discounts[customer_type] %||% 0.0
  if (is_holiday) {
    discount <- discount + 0.1
  }
  return(discount)
}

To reduce cyclomatic complexity:

  • Reduce branching or pathways in the code.
  • Bring related code together, and keep unrelated code apart.
  • Look for blocks of code that can be separated in methods or functions; this is known as method extraction.

4. Composing methods

At the last step, focus on improving the structure and readability of the code by extracting methods or functions from existing code blocks. This involves breaking down large methods into smaller, more manageable pieces that each perform a single task or function.

When composing methods, do the following:

  • Make each extracted method (or function) tell its own story. This requires understanding the context of the code within a program and how it is expected to be read and interpreted by other developers.
  • Ideally, each method tells a single, well-structured and easy-to-understand story. If that is not the case, the code is poorly written and should be refactored.
  • Rename things (functions, classes, variables) so that their behaviour is clear in the code.

Footnotes

  1. Farley, D. (n.d.) Refactoring legacy code (Online Course). CD.Training. https://courses.cd.training/courses/refactoring-tutorial↩︎