Key Takeaways
1. Legacy Code: Code Without Tests is Bad Code
Code without tests is bad code.
Defining Legacy Code. Legacy code isn't just old code or code written by someone else; it's code that's difficult to change and understand, primarily because it lacks tests. This definition emphasizes the practical challenges of working with such code, regardless of its age or origin.
The Importance of Tests. Tests provide a safety net, allowing developers to confidently modify and refactor code without fear of introducing unintended consequences. Without tests, changes become risky and time-consuming, leading to stagnation and technical debt. Tests enable quick and verifiable changes, ensuring code improves rather than degrades.
Clean Code Isn't Enough. While clean, well-structured code is desirable, it's not a substitute for tests. Even the most elegant code can harbor hidden bugs, and without tests, these bugs can be difficult to detect and fix. Tests provide a crucial layer of verification that clean code alone cannot offer.
2. The Mechanics of Change: Structure, Functionality, and Resources
Behavior is the most important thing about software.
Three Dimensions of Change. When modifying software, three primary aspects are subject to change: structure (the organization of the code), functionality (what the code does), and resource usage (time, memory, etc.). Understanding these dimensions helps developers manage the impact of their changes.
Preserving Behavior. The biggest challenge in software development is preserving existing behavior while making changes. Whether adding features, fixing bugs, refactoring, or optimizing, the goal is to minimize unintended side effects and maintain the stability of the system. This requires a deep understanding of the code and a way to verify that changes are correct.
Risk Management. To mitigate risk, developers must ask three key questions: What changes need to be made? How will we know they've been done correctly? How will we know that we haven't broken anything? Answering these questions helps to focus efforts and prioritize testing.
3. Feedback Loops: Edit and Pray vs. Cover and Modify
Effective software change, like effective surgery, really involves deeper skills.
Two Approaches to Change. The book contrasts two approaches to changing code: "Edit and Pray," where changes are made with the hope of correctness, and "Cover and Modify," where changes are made with a safety net of tests. The latter approach is presented as more effective and professional.
The Power of Unit Testing. Unit testing is highlighted as a crucial component of legacy code work. Small, localized tests provide rapid feedback, allowing developers to refactor with greater safety and confidence. This contrasts with system-level regression tests, which can be slow and difficult to debug.
Testing to Detect Change. The book advocates for "testing to detect change" rather than "testing to attempt to show correctness." This means using tests to identify unintended side effects of code modifications, ensuring that existing behavior is preserved. This approach is also known as regression testing.
4. Sensing and Separation: Breaking Dependencies for Testability
Dependency is one of the most critical problems in software development.
Two Reasons to Break Dependencies. When working with legacy code, dependencies often hinder testability. The book identifies two primary reasons to break dependencies: sensing (gaining access to values computed by the code) and separation (isolating code for testing).
Faking Collaborators. One common technique for sensing is to use fake objects, which impersonate collaborators of the class being tested. This allows developers to verify interactions and outputs without relying on real dependencies. Mock objects are a more advanced type of fake that perform assertions internally.
The Importance of Testability. The ability to sense and separate code is crucial for effective testing and refactoring. By breaking dependencies, developers can gain control over their code and make changes with greater confidence. This is a key step in taming legacy code.
5. The Seam Model: Altering Behavior Without Editing in Place
A seam is a place where you can alter behavior in your program without editing in that place.
Programs as Sheets of Text. The book challenges the traditional view of programs as monolithic sheets of text, arguing that this perspective hinders testability and maintainability. Instead, it introduces the concept of seams, which are points in the code where behavior can be altered without direct editing.
Types of Seams. The book identifies several types of seams, including preprocessing seams (using macros), link seams (substituting libraries), and object seams (using polymorphism). Each type of seam offers different opportunities for altering behavior under test.
Enabling Points. Every seam has an enabling point, a place where the decision to use one behavior or another is made. Understanding enabling points is crucial for exploiting seams and controlling behavior during testing. Object seams are the most useful seams available in object-oriented programming languages.
6. The Legacy Code Change Algorithm: A Step-by-Step Approach
At the end of each programming episode, we should be able to point not only to code that provides some new feature, but also its tests.
A Structured Approach. The book presents a five-step algorithm for making changes in legacy code: identify change points, find test points, break dependencies, write tests, and make changes and refactor. This algorithm provides a structured approach to taming unruly code bases.
Identifying Change and Test Points. The algorithm emphasizes the importance of identifying both the areas where changes need to be made and the points where tests can be written to verify those changes. This requires a deep understanding of the code and its dependencies.
Breaking Dependencies and Writing Tests. Breaking dependencies is often the most challenging step in the algorithm. The book provides a catalog of techniques for breaking dependencies, allowing developers to isolate code for testing. Once dependencies are broken, tests can be written to characterize existing behavior and verify new functionality.
7. Time Management: Balancing Speed and Safety
Remember, code is your house, and you have to live in it.
The Time Crunch Dilemma. The book acknowledges the pressure to deliver features quickly, but argues that taking the time to write tests upfront can save time and frustration in the long run. This requires a shift in mindset and a willingness to invest in code quality.
Sprout and Wrap Techniques. When time is limited, the book suggests using "sprout" and "wrap" techniques to add new functionality without modifying existing code. These techniques allow developers to add tested code while minimizing the risk of introducing bugs.
The Importance of Testing. Ultimately, the book emphasizes the importance of testing as a way to improve code quality and reduce the risk of introducing bugs. While it may take time upfront, testing can lead to a more maintainable and reliable system in the long run.
8. Feature Addition: Sprout and Wrap Techniques
Code is your house, and you have to live in it.
Adding Features Safely. When adding features to legacy code, it's crucial to do so in a way that minimizes risk and preserves existing behavior. The book introduces two key techniques for achieving this: Sprout Method and Wrap Method.
Sprout Method. This technique involves creating a new method to encapsulate the new functionality and calling it from the existing code. This allows developers to add tested code without modifying the original method.
Wrap Method. This technique involves renaming the original method and creating a new method with the original name that calls both the new functionality and the original method. This allows developers to add behavior to existing calls of the original method.
9. Dependency Resolution: Getting Classes into Test Harnesses
When we change code, we should have tests in place. To put tests in place, we often have to change code.
The Legacy Code Dilemma. One of the biggest challenges in working with legacy code is the "legacy code dilemma": to change code, you need tests, but to put tests in place, you often have to change code. This creates a chicken-and-egg problem that can be difficult to overcome.
Common Dependency Problems. The book identifies four common problems that hinder testability: difficulty creating objects, difficulty building test harnesses, problematic constructor side effects, and the need to sense constructor behavior.
Dependency-Breaking Techniques. To address these problems, the book introduces a variety of dependency-breaking techniques, such as Primitivize Parameter, Extract Interface, and Introduce Static Setter. These techniques allow developers to isolate code for testing without making drastic changes to the system.
10. Effect Analysis: Reasoning About Code Impact
Preserving existing behavior is one of the largest challenges in software development.
Understanding Code Effects. Before making changes to legacy code, it's crucial to understand the potential impact of those changes. This involves reasoning about the effects of code modifications and identifying areas that may be affected.
Effect Sketches. The book introduces a technique called "effect sketching," which involves creating diagrams to visualize the relationships between code elements and their potential impact. This helps developers to identify areas that need to be tested.
Targeted Testing. By understanding the effects of code changes, developers can focus their testing efforts on the areas that are most likely to be affected. This allows them to write targeted tests that provide the greatest value.
11. Refactoring Strategies: From Monster Methods to Manageable Code
When you break dependencies in legacy code, you often have to suspend your sense of aesthetics a bit.
Tackling Monster Methods. Monster methods, or extremely long and complex methods, are a common problem in legacy code. The book provides strategies for breaking down these methods into smaller, more manageable pieces.
Automated Refactoring Support. When available, automated refactoring tools can be invaluable for breaking down monster methods. These tools can safely extract methods and perform other refactorings, reducing the risk of introducing bugs.
Manual Refactoring Techniques. When automated tools are not available, the book provides manual techniques for refactoring monster methods, such as Introduce Sensing Variable and Extract What You Know. These techniques allow developers to make incremental improvements to the code while minimizing risk.
12. Risk Mitigation: Editing with Awareness and Precision
The amount of behavior that we have to preserve is usually very large, but that isn’t the big deal. The big deal is that we often don’t know how much of that behavior is at risk when we make our changes.
Hyperaware Editing. The book emphasizes the importance of being aware of the potential impact of every keystroke when editing code. This requires a deep understanding of the code and a focus on minimizing unintended side effects.
Single-Goal Editing. To reduce risk, the book recommends focusing on one goal at a time when editing code. This helps to prevent scope creep and ensures that changes are made deliberately and with a clear purpose.
Preserve Signatures. When refactoring code, it's important to preserve the signatures of methods whenever possible. This reduces the risk of introducing bugs and makes it easier to verify that changes are correct.
Last updated:
Review Summary
Working Effectively with Legacy Code is highly regarded as a essential guide for dealing with untested code. Readers praise its practical techniques for introducing tests and breaking dependencies, though some find it dated. The book's definition of legacy code as untested code resonates with many. While the focus on Java and C++ limits its applicability for some, developers appreciate the insights on refactoring, understanding complex systems, and maintaining developer sanity. Overall, it's considered valuable for anyone working with existing codebases, despite some repetition and outdated examples.
Similar Books









