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:
FAQ
What is Working Effectively with Legacy Code by Michael C. Feathers about?
- Focus on legacy code: The book addresses the challenges of working with legacy code—code without tests that is difficult to change safely.
- Practical techniques: It provides actionable strategies for understanding, testing, and refactoring legacy systems, emphasizing incremental improvement.
- Dependency-breaking: A major theme is breaking dependencies to get code under test, enabling safer and more confident changes.
- Real-world orientation: The book is grounded in real-world software maintenance, offering advice that applies to the messy, complex codebases developers actually encounter.
Why should I read Working Effectively with Legacy Code by Michael C. Feathers?
- Solves real-world problems: Most developers face legacy code, and the book offers proven methods to make maintenance and enhancement tasks more manageable.
- Reduces risk and fear: By learning to write tests and break dependencies, you can change legacy systems with less risk of introducing bugs.
- Improves productivity: The techniques help teams work faster and with more confidence, leading to healthier, more maintainable codebases.
- Comprehensive guidance: It covers a wide range of topics, from identifying change points to advanced refactorings, making it valuable for developers, leads, and managers.
How does Michael C. Feathers define "legacy code" in Working Effectively with Legacy Code and why is this definition important?
- Code without tests: Feathers defines legacy code as any code without tests, regardless of its age or origin.
- Shifts the focus: This definition moves the conversation from code age or quality to the presence of tests, highlighting their critical role.
- Tests as safety net: Without tests, changes are risky and slow because developers can't be sure they aren't breaking existing behavior.
- Guides improvement: The definition underpins the book’s approach—getting code under test is the first and most crucial step to safe evolution.
What are the key concepts and techniques in Working Effectively with Legacy Code by Michael C. Feathers?
- Seams and fakes: The book introduces seams—places where you can change behavior without editing code at that point—and fake objects to isolate code for testing.
- Legacy Code Change Algorithm: A stepwise process for making safe changes: identify change points, find test points, break dependencies, write tests, then refactor.
- Dependency-breaking techniques: Includes Extract Interface, Parameterize Constructor, Subclass and Override Method, and more to enable testing.
- Characterization tests: Tests that capture current behavior, providing a baseline for safe refactoring and modification.
What is the "Legacy Code Change Algorithm" in Working Effectively with Legacy Code and how does it work?
- Five-step process: The algorithm involves identifying change points, finding test points, breaking dependencies, writing tests, and then making changes and refactoring.
- Incremental improvement: The goal is to gradually increase the amount of code under test, turning isolated "islands" of tested code into larger "continents."
- Interlinked techniques: Each step may require various dependency-breaking and refactoring methods, which are detailed throughout the book.
- Safe, stepwise change: The algorithm is designed to minimize risk and maximize safety when working with difficult code.
What are "seams" in Working Effectively with Legacy Code and why are they crucial for legacy code work?
- Definition of seams: A seam is a place where you can alter program behavior without editing code at that location.
- Types of seams: The book describes preprocessing seams (macros), link seams (dynamic linking), and object seams (polymorphic calls).
- Enable testing: Seams allow developers to replace or fake collaborators, making it possible to isolate code for testing.
- Break dependencies: By exploiting seams, you can break dependencies that otherwise make legacy code hard or impossible to test.
How does Working Effectively with Legacy Code by Michael C. Feathers recommend handling dependencies on third-party libraries or APIs?
- Avoid direct calls: The book advises against scattering direct library calls throughout your code to prevent inflexibility.
- Use wrappers and interfaces: Create thin wrappers or interfaces around library classes, especially when dealing with final or non-virtual methods.
- Skin and Wrap vs. Responsibility-Based Extraction: For simple APIs, skin and wrap with interfaces; for complex APIs, extract responsibilities into new methods or classes.
- Facilitates testing: These techniques isolate dependencies, making it easier to substitute fakes or mocks during testing.
What are characterization tests in Working Effectively with Legacy Code and how do they help?
- Document current behavior: Characterization tests capture what the code actually does, not what it’s supposed to do.
- Safety net for change: They provide a baseline, allowing you to refactor or modify code without fear of breaking existing functionality.
- Writing approach: Write a failing test, observe actual behavior, then adjust the test to match, iteratively building a suite that characterizes the code.
- Essential for legacy code: Especially useful when original requirements or documentation are missing.
How does Working Effectively with Legacy Code by Michael C. Feathers suggest understanding and working with large, complex legacy systems?
- Tell the system’s story: Articulate a simple, shared narrative of the system’s architecture and responsibilities to guide changes.
- Naked CRC and conversation scrutiny: Use index cards to represent objects and their interactions, and listen to team discussions to surface natural concepts.
- Sketching and markup: Informal sketches and annotated code listings help visualize relationships and the impact of changes.
- Incremental comprehension: These practices help teams gradually build a clearer mental model of complex codebases.
What strategies does Working Effectively with Legacy Code by Michael C. Feathers recommend for dealing with big classes and monster methods?
- Single Responsibility Principle: Break big classes into smaller ones, each with a single responsibility, to improve testability and maintainability.
- Identify responsibilities: Group methods by similarity, examine private methods, and look for clusters of related variables and methods.
- Refactoring tactics: Use Sprout Method and Sprout Class to add new functionality, and Extract Method to break down large methods.
- Incremental extraction: Refactor in small, safe steps, ideally supported by tests.
What are the most important dependency-breaking techniques in Working Effectively with Legacy Code by Michael C. Feathers?
- Adapt Parameter: Replace complex parameters with simpler interfaces or wrappers to ease testing.
- Break Out Method Object: Move large methods into their own classes to manage complexity and enable focused testing.
- Encapsulate Global References: Wrap global variables or functions in classes to control access and allow substitution in tests.
- Extract and Override Methods: Extract calls or object creation into overridable methods for easier substitution in tests.
- Introduce Static Setter: Add static setters to singletons to allow replacement with fakes during testing, even if somewhat invasive.
How does Working Effectively with Legacy Code by Michael C. Feathers recommend safely refactoring legacy code without existing tests?
- Preserve method signatures: When extracting or moving code, copy entire method signatures to avoid errors and maintain behavior.
- Lean on the compiler: Use compiler errors to guide and validate changes, reducing the chance of mistakes.
- Use sensing variables: Introduce temporary variables to detect if certain code paths are exercised, enabling incremental testing.
- Scratch refactoring: Experiment with exploratory refactoring on a copy of the code to understand structure and dependencies before making real changes.
What mindset and practices does Working Effectively with Legacy Code by Michael C. Feathers encourage for effective legacy code work?
- Hyperaware, single-goal editing: Be conscious of each change’s impact and focus on one task at a time to reduce errors.
- Pair programming: Collaborate with a partner to catch mistakes early and share knowledge.
- Incremental improvement: Favor small, safe changes supported by tests over large-scale rewrites.
- Find motivation: Connect with the craft of programming, work with supportive teams, and engage with the community to overcome overwhelm and stay motivated.
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










Download PDF
Download EPUB
.epub
digital book format is ideal for reading ebooks on phones, tablets, and e-readers.