Refactoring Legacy Code
This is a free video tutorial on refactoring legacy code by Continuous Delivery Training.
It covers techniques and best practices for improving and maintaining legacy codebases, making them more manageable and easier to work with.
Continuous Delivery Training
Continuous Delivery Training offers a variety of courses focused on software development practices, including Continuous Integration, Continuous Delivery, DevOps, and Agile methodologies.
Their courses are designed to help developers and teams improve their skills and adopt best practices in software development.
Sign up and access the tutorial
Navigate to the list of courses and select the Refactoring Legacy Code tutorial.
Enroll for free to create an account and access the Refactoring Legacy Code tutorial.

Note: The Continuous Delivery Training leaves your enrollment active for 1 month, which is usually enough time to complete the tutorial.
GitHub Repository
The code examples for the tutorial are available on GitHub as a template repository:
GitHub - davef77/RefactoringBadCode: Starting point for an exercise in refactoring bad code
You can generate a new repository with the same directory structure and files as the template.
See GitHub’s documentation for Creating a repository from a template.
Four Steps to Refactoring
Dave outlines four key steps to refactoring legacy code safely:
- Removing Clutter
- Reducing Complexity
- Composing Methods
- Refactoring to Testability
Part 1: Approval Testing and Removing Clutter
Approval Testing
Approval Testing is a technique used to verify that the behavior of a system remains unchanged after refactoring.
It involves capturing the current output of the system and comparing it to the output after changes have been made.
How it works:
- Capture the output of the code under test.
- Save this “approved” output to disk.
- On subsequent runs, compare the new output to the approved one.
- If they match, the test passes. If not, the test fails.
This is especially useful when dealing with legacy code, where the original intent and functionality may not be well documented and traditional unit testing is difficult.
Dave demonstrates:
- Creating an approval test for a class
XMLToJSON. - Using sample input data based on comments in the original code.
- Running the test to confirm expected behavior.
Removing Clutter
The first step in refactoring is cleaning up unnecessary code and comments:
- Delete redundant comments: Those that merely restate what the code does.
- Remove dead code: Unused or commented-out sections.
- Simplify variable names: Rename variables to be self-explanatory (e.g.,
url→urlToTOC). - Extract small methods: For clarity, such as converting inline logic into helper methods (e.g.,
hasChildren()).
Always run your tests after each change to ensure stability. This practice confirms that refactoring has not altered expected behavior, and provides confidence to proceed with further improvements.
Automated tests act as a safety net, allowing you to make incremental changes and quickly identify issues, making the refactoring process safer and more efficient.
Summary & Key Lessons:
- Approval Testing provides a strong safety net for refactoring legacy code without changing behavior.
- Removing Clutter improves readability, reduces noise, and clarifies structure.
- Version Control is essential for maintaining stability through small, incremental changes.
- Work in Tiny Steps: Commit after each successful test to maintain a safe rollback point.
Part 2: Reducing Complexity and Composing Methods
Reducing Complexity
Once you’ve cleared away obvious clutter, focus on reducing cyclomatic complexity — the number of paths through your code. Deeply nested loops and conditionals often signal that your code is too complex.
The key technique here is Extract Method. Identify related blocks of code, and move them into their own methods. For example:
- A block setting up a document node.
- A loop processing elements.
- A block closing the JSON string.
Each extraction simplifies the main method, turning tangled logic into a sequence of clear steps.
Refactoring is about small, reversible improvements. After every working change, commit your code. Even if you stop early, the code should already be in a better state.
Follow the Boy Scout Rule: leave the code cleaner than you found it.
Composing Methods
Once complexity is reduced, focus on composing methods — give each method a descriptive name and organize them so that each one clearly describes what it does. This helps clarify intent and makes the code easier to reason about.
For instance, a getJsonForDoc() method might have three clear steps:
- Get the node to parse using
getNode(). - Process each element with
processElement(). - Close the JSON string via
closeJson().
This makes the method self-documenting. You no longer need verbose comments — the structure and names tell the whole story.
Summary & Key Lessons:
- Reduce cyclomatic complexity by extracting methods and naming them clearly.
- Use small, safe steps — refactor incrementally and test often.
- Leverage simple refactorings like Rename and Extract Method for big impact.
- Compose methods to tell a story — make your code self-documenting.
- Leave the code better than you found it every time you make a change.
- Aim for progress, not perfection — good refactoring is incremental, not revolutionary.
Part 3: Refactoring to Testability
This episode concludes the series on refactoring legacy code. Earlier parts covered the first three steps of his four-step process: removing clutter, reducing cyclomatic complexity, and composing methods. The final step takes these improvements further by introducing automated tests — and seeing how they immediately surface hidden bugs.
Dave begins by introducing some further refactorings to the codebase to improve testability. I’m providing my own summary of the key techniques he demonstrates.
Replace Function with Command
Replace Function with Command aka Replace Method with Method Object encapsulates behavior in command objects for better testability.
- Extract method
toJsonString(String, Element)to convert a Document element to JSON string. - Create class
DocumentElementto represent document elements - encapsulates related data and behavior. - Add
DocumentElementas parameter totoJsonStringmethod. - Inline method
hasChildren- make the code accessible fromtoJsonStringmethod. - Convert
toJsonStringmethod to an instance method ofDocumentElement- improve encapsulation and support polymorphism. - Extract method
hasChildreninDocumentElementclass. - Create constructor
DocumentElement(String)- pass arguments to the command on the constructor. - Use
xPathStringfield intoJsonStringmethod - utilize instance fields for better encapsulation. - Safe delete
xPathStringparameter fromtoJsonStringmethod - remove unused parameters to clean up the method signature. - Add
Elementparameter toDocumentElementconstructor - ensure all necessary data is provided at instantiation. - Use
elementfield intoJsonStringmethod - utilize instance fields for better encapsulation. - Safe delete
elementparameter fromtoJsonStringmethod - further clean up the method signature. - Introduce field
jsonStringto store the result oftoJsonStringmethod - maintain state within the command object. - Introduce field
elementNameto store the element name - move the function state into the command object. - Introduce field
attributesto store the element attributes - move the function state into the command object. - Introduce field
titleto store the element title - move the function state into the command object. - Introduce field
fileto store the element filename - move the function state into the command object. - Extract method
processDocAttributesinDocumentElementclass - break downtoJsonStringmethods into smaller pieces. - Extract method
addStateClosedinDocumentElementclass - compose method to handle state closing logic. - Extract method
closeElementinDocumentElementclass - compose method to handle element closing logic. - Extract method
processFolderAttributesinDocumentElementclass - break downtoJsonStringmethods into smaller pieces.
Replace Conditional with Polymorphism
Refactoring technique to eliminate conditionals based on type codes by using subclasses and polymorphism.
- Extract method
processElement()inDocumentElementclass - extract conditional statement into its own method. - Encapsulate variable
elementName- self-encapsulate the type code. - Replace Constructor with Factory Function aka Replace Constructor with Factory Method - use factory methods to
create an instance of
DocumentElement. - Create
DocElementsubclass and add selector logic fordoctype code value in the factory method. -
Override the type code getter in
DocElementclass to return the literal type code value.Ensure the subclass is being used by altering the return value of the
getElemName()override and testing again to ensure the test fails. Confirm success by restoring the correct return value and running the test again. - Create
FolderElementsubclass and add selector logic forfoldertype code value in the factory method. - Override the type code getter in
FolderElementclass to return the literal type code value. - Remove type code field
elementNamefromDocumentElementclass - eliminate redundant type code field. - Add default branch in the factory method to handle unexpected type code values.
- Make
getElementNamean abstract method andDocumentElementan abstract class - enforce implementation in subclasses. - Override
processElementmethod inDocElementclass. Copy the relevant logic from the conditional statement. - Override
processElementmethod inFolderElementclass. Copy the relevant logic from the conditional statement. - Declare
processElementmethod inDocumentElementclass as abstract - enforce implementation in subclasses. - Safe delete
getElementNamemethod fromDocumentElementclass and its subclasses - clean up unused methods. - Push Down Method
hasChildrentoDocElementclass - move method to the subclass where it’s relevant.