library / lib66a960814af19548
Working Effectively with Legacy Code
Michael C. Feathers · 2004
In a sentence
A battle-tested field guide for software developers who must safely change, test, and improve code they didn't write and can barely understand.
Working Effectively with Legacy Code by Michael C. Feathers is the definitive handbook for every developer who has inherited a codebase that is tangled, untested, and terrifying to change. Feathers redefines legacy code as simply 'code without tests,' then delivers a comprehensive, language-agnostic toolkit for breaking the vicious cycle: you need tests to change code safely, but you have to change code to get tests in place. Through dozens of real-world scenarios organized as FAQs—'I can't get this class into a test harness,' 'I need to change a monster method,' 'my application has no structure'—he teaches developers how to identify seams (places where behavior can be altered without editing that place), break dependencies just enough to get code under test, write characterization tests that pin down existing behavior, and then refactor with confidence. The book covers Java, C++, C, and C# with concrete dependency-breaking techniques such as Extract Interface, Parameterize Constructor, Subclass and Override Method, and more than two dozen others, all designed to be performed safely without a full test suite already in place. Whether you are facing a 10,000-line monster method, a singleton-riddled codebase, or a system that takes forever to build, this book gives you the courage, the concepts, and the concrete steps to move forward.
The four lenses
- Science
- Statistics
- Systems
- Strategy
Tags
The model
A causal model describing how structural properties of a codebase and developer practices drive dependency breakability, testability, and behavioral safety during software change in legacy systems.
Seam Densitydesign lever
The degree to which a codebase contains places where behavior can be varied without editing that place, including object seams (polymorphic dispatch), link seams (substitutable libraries), and preprocessing seams (macro or include substitution). Higher seam density means more leverage points for isolating units under test.
Dependency Couplingcontextual condition
The extent to which classes and functions in the system are tightly coupled to concrete collaborators, global variables, singletons, or external resources (databases, hardware, network) in ways that prevent independent instantiation and execution in a test harness. High coupling impedes sensing and separation.
Test Coveragecontextual condition
The proportion of production code behaviors—methods, branches, and value-generating paths—that are exercised and verified by automated tests in the test harness. In the book's framework this encompasses both characterization tests that preserve existing behavior and new tests that specify intended behavior.
Dependency-Breaking Practicedesign lever
The regularity and skill with which developers apply dependency-breaking techniques (Extract Interface, Parameterize Constructor, Subclass and Override Method, etc.) to introduce seams and enable testing. This is a behavioral practice variable reflecting team capability and discipline.
Characterization Test Writingbehavioral pattern
The practice of writing tests that document and preserve the actual current behavior of code before making changes, rather than tests written to find bugs or specify ideal behavior. This practice creates a behavioral regression net around change points.
Change Fearpsychological state
The psychological state of apprehension, risk aversion, and reluctance to make changes in the codebase experienced by developers, arising from uncertainty about downstream effects of edits in an untested system. Change fear leads to avoidance behaviors that cause further code degradation.
Editing Disciplinebehavioral pattern
The behavioral practice of making one change at a time (single-goal editing), preserving method signatures during dependency-breaking refactorings, using the compiler as a navigation tool, and pairing with another developer to catch errors. Discipline reduces the probability of introducing new defects during change.
Class Size and Responsibility Sprawlcontextual condition
The degree to which individual classes accumulate multiple distinct responsibilities, large numbers of methods, and extensive instance variable sets, making them difficult to understand, test, and change. This is a negative structural property correlated with high dependency coupling and low testability.
Testabilitycontextual condition
The ease with which individual classes and methods can be instantiated in a test harness, exercised with controlled inputs, and observed for their effects, without requiring excessive setup, real external resources, or extensive dependency satisfaction. Testability is the proximate enabler of test coverage.
Safe Changeabilityoutcome metric
The degree to which developers can make intended functional changes to the codebase quickly and with high confidence that existing behaviors are preserved, measured by low post-change defect rates, short feedback cycles, and developer confidence. This is the primary proximal outcome of the book's approach.
Code Quality Trendoutcome metric
The directional trajectory of codebase health over time, reflecting whether the system is improving (becoming more modular, better tested, easier to understand) or degrading (accumulating technical debt, growing class sizes, losing structure). The book's primary long-run outcome.
Build and Test Feedback Speedcontextual condition
The elapsed time between a developer making a code change and receiving reliable automated feedback (compilation plus test execution) about whether the change preserved existing behavior. Shorter feedback loops enable more iterative, confident development.
Understanding of Codepsychological state
The degree to which developers comprehend the structure, responsibilities, effects, and intended behavior of the code they are working in, as acquired through techniques such as effect sketching, feature sketching, scratch refactoring, listing markup, and story-telling sessions.
How they connect
- dependency coupling − influences testability
- seam density → influences testability
- dependency breaking practice → predicts seam density
- dependency breaking practice → predicts testability
- testability → predicts test coverage
- characterization test writing → predicts test coverage
- test coverage − influences change fear
- change fear − influences safe changeability
- test coverage → predicts safe changeability
- editing discipline → predicts safe changeability
- build feedback speed − influences change fear
- build feedback speed → influences safe changeability
- class size and responsibility sprawl → influences dependency coupling
- class size and responsibility sprawl − influences testability
- understanding of code → predicts characterization test writing
- safe changeability → predicts code quality trend
- test coverage → predicts code quality trend
- dependency coupling − influences build feedback speed
The process
This book provides a comprehensive playbook for working with legacy code, focusing on a disciplined, iterative approach to safely introduce changes, improve quality, and add new features. The core philosophy is that no code modification should occur without a safety net of tests. The overall process begins with understanding the organizational and technical context, fostering a positive team culture, and using exploratory techniques to comprehend the existing codebase. The central loop of the playbook involves getting the code under test, which often requires systematically identifying and breaking dependencies using a variety of specialized techniques. Once a test harness is established, characterization tests are written to document the code's actual behavior, creating a baseline for safe refactoring. With this safety net, developers can then apply a range of refactoring patterns to improve the code's structure, reduce complexity, and eliminate duplication. Finally, with a more maintainable and well-tested codebase, new functionality can be added confidently using methodologies like Test-Driven Development (TDD). This entire cycle is supported by disciplined editing practices and strategic use of automation to manage risk and enhance productivity when evolving large, complex systems.
Managing Organizational Change for Legacy Systems
To initiate, implement, and sustain technical and process changes within an organization by overcoming inertia and breaking down dependencies.
When to use: When beginning a large-scale legacy code improvement initiative or when facing organizational resistance to change.
Step 1Acknowledge existing lag time and identify specific areas for improvement.
Entry: Leadership recognizes the need for change.
Exit: A list of key dependencies and improvement areas is created.
In: Understanding of current organizational dynamics · Out: Inventory of dependencies, List of improvement areas
ch06 · ch07
Step 2Engage stakeholders and develop a communication strategy to align them with the change vision.
Entry: Improvement areas and dependencies are identified.
Exit: Stakeholders are informed and a communication plan is active.
In: Stakeholder feedback · Out: Communication plan
ch07
Step 3Implement small, manageable changes to build momentum and gather quick wins.
Entry: Stakeholders are aligned on the need for change.
Exit: Initial changes are successfully implemented and communicated.
- Which quick wins to pursue first.
In: List of improvement areas · Out: Visible quick wins, Increased momentum for change
ch06 · ch07
Step 4Establish feedback loops and ongoing communication to sustain the change.
Entry: Initial changes have been implemented.
Exit: A continuous improvement cycle is established.
- Whether feedback indicates success or necessitates adjustments.
In: Feedback from implemented changes · Out: Sustained changes, Culture of continuous improvement
ch06 · ch07
Step 5Celebrate successes and create a sustainability plan to ensure agility remains integral.
Entry: Momentum has been built and feedback loops are active.
Exit: Change is embedded in the organization's culture.
Out: Sustainability plan
ch07
Fostering a Positive Legacy Code Culture
To improve team morale, motivation, and productivity when working with challenging legacy systems.
When to use: When team morale is low due to the difficulty of working with a legacy codebase.
Step 1Reflect on personal motivations and identify enjoyable aspects of programming to rediscover.
Entry: A feeling of demotivation or frustration with the current work.
Exit: Renewed personal engagement with the work.
In: Personal reflection · Out: Enhanced personal motivation
ch25
Step 2Engage with coworkers and the broader programming community to foster a supportive environment.
Exit: A stronger support network is established.
Out: Supportive work environment
ch25
Step 3As a team, identify the most problematic areas of the codebase to improve collectively.
Entry: The team acknowledges that code quality is impacting morale.
Exit: A prioritized list of code areas for improvement is created.
- Which sections of the codebase to prioritize for improvement.
In: Team feedback on codebase pain points · Out: Prioritized list of refactoring targets
ch25
Step 4Implement testing and refactoring strategies on the identified code sections and celebrate progress.
Entry: Refactoring targets have been identified.
Exit: Code quality is improved and the team feels a sense of achievement.
In: Prioritized list of refactoring targets · Out: Improved code quality, Enhanced team morale
ch25
Code Comprehension and Exploration
To build a mental model of complex or unfamiliar code through various exploratory and documentation techniques, without making permanent changes.
When to use: Before attempting to refactor or add features to a complex area of the codebase.
Step 1Use sketching and note-taking to visualize relationships between code elements.
Entry: Need to understand a section of code.
Exit: A visual map of the code's structure is created.
In: Source code · Out: Sketches and notes clarifying code structure
ch18
Step 2Apply listing markup to printed code to identify responsibilities and structure.
Entry: A long or complex method needs to be understood.
Exit: An annotated printout that clarifies the method's internal structure.
- Choice of focus for markup (e.g., responsibilities, structure, change impact).
In: Printed source code · Out: Annotated code with enhanced clarity
ch18
Step 3Perform scratch refactoring to explore the code without risk.
Entry: A deep, hands-on understanding of the code is needed.
Exit: Increased comprehension of the code's structure and behavior.
In: Source code from version control · Out: Insights into code structure
ch18
Step 4Identify and delete unused code to reduce clutter.
Entry: The codebase is cluttered with obsolete or dead code.
Exit: A cleaner, more focused codebase.
- Whether to keep or remove code based on a utility assessment.
In: Source code, Version control system · Out: A cleaner codebase
ch18
Organizing Production and Test Code
To establish clear naming conventions and directory structures for test code to improve navigability, maintainability, and deployment.
When to use: When setting up a new project or when an existing project's test code has become disorganized.
Step 1Establish consistent naming conventions for test classes and test doubles.
Entry: A decision is made to standardize test code organization.
Exit: A team-wide naming convention is adopted.
- Choosing between a prefix or suffix for test classes.
In: List of production class names · Out: Clearly named test classes
ch19
Step 2Evaluate deployment constraints to determine the test code location strategy.
Entry: Naming conventions are established.
Exit: A decision on test code location is made.
- Whether to co-locate test and production code or place them in separate directory trees.
In: Deployment constraints information · Out: Test location strategy
ch19
Step 3Implement the chosen directory structure.
Entry: A location strategy has been chosen.
Exit: The project's directory structure is organized according to the strategy.
Out: Organized project directory structure
ch19
Step 4Configure the build system to exclude test code from final deployments.
Entry: Directory structure is implemented.
Exit: Production builds are free of test code.
Out: Clean production artifact
ch19
Establishing a Test Harness for Legacy Code
To create a structured framework for writing and executing automated tests for legacy code, enabling safe refactoring and feature development.
When to use: When starting to work on a legacy codebase with the goal of improving its quality and maintainability.
Step 1Choose an appropriate testing framework (e.g., xUnit) and integrate it into the development environment.
Entry: A decision has been made to add automated tests to the project.
Exit: The testing framework is configured and accessible in the project.
- Selection of a specific testing framework.
In: Development environment · Out: Integrated testing framework
ch05 · ch09p02
Step 2Identify the legacy class or unit of work that needs to be tested.
Entry: Testing framework is in place.
Exit: A target for testing is selected.
In: Legacy codebase · Out: Selected test target
ch09p01 · ch09p03
Step 3Write a construction test to attempt to instantiate the class in the test harness.
Entry: A test target is selected.
Exit: Compiler or runtime feedback on instantiation issues is available.
In: Legacy class · Out: Compiler feedback, List of dependencies
ch09p03
Step 4Use dependency-breaking techniques to resolve instantiation issues.
Entry: Instantiation issues have been identified.
Exit: The class can be successfully instantiated within a test.
- Which dependency-breaking technique to apply.
In: List of dependencies · Out: A testable class
ch09p01 · ch09p03
Step 5Write unit tests using the framework's assertion methods to validate behavior.
Entry: The class can be instantiated in a test.
Exit: A suite of tests covering the class's behavior is created.
In: Testable class · Out: Unit tests
ch09p02
Step 6Integrate test execution into the development cycle, such as in a continuous integration (CI) pipeline.
Entry: A suite of tests exists.
Exit: Tests are run automatically on a regular basis.
- When to run tests (e.g., on every commit, nightly).
In: Unit tests · Out: Automated testing process
ch05
Characterization Testing
To document the actual, current behavior of legacy code through tests, creating a safety net before making changes.
When to use: Before refactoring or modifying any piece of legacy code that lacks tests.
Step 1Place the piece of code to be characterized into a test harness.
Entry: A test harness is available for the code.
Exit: The code is callable from a test method.
In: Legacy code, Test harness
ch17
Step 2Write an assertion that you expect to fail.
Entry: The code is in a test harness.
Exit: A failing test is written.
Out: A failing test
ch17
Step 3Run the test and observe the failure to learn the code's actual behavior.
Entry: A failing test has been written.
Exit: The actual behavior of the code is understood.
In: Failing test · Out: Knowledge of actual code behavior
ch17
Step 4Modify the test's assertion to match the actual behavior, making the test pass.
Entry: The actual behavior is understood.
Exit: A passing test that documents the current behavior exists.
- If the behavior is unexpected, decide whether to document it as is or investigate it as a potential bug.
In: Knowledge of actual code behavior · Out: Passing characterization test
ch17
Step 5Repeat the process for different inputs and conditions until the behavior is sufficiently characterized.
Entry: An initial characterization test is passing.
Exit: A suite of characterization tests covers the area to be modified.
Out: Characterization test suite
ch17
Breaking Dependencies for Testability
To modify code so that classes or functions can be instantiated and tested in isolation from their dependencies (e.g., databases, GUIs, other complex classes).
When to use: When a class cannot be instantiated or a method cannot be tested without pulling in a large number of other components.
Step 1Identify the problematic dependency that prevents testing.
Entry: An attempt to write a test for a piece of code has failed due to a dependency.
Exit: The specific dependency and coupling point are identified.
In: Untestable code · Out: Identified problematic dependency
ch09p01 · ch10 · ch11 · ch12
Step 2Select an appropriate dependency-breaking technique based on the type of coupling.
Entry: The dependency is identified.
Exit: A suitable technique is chosen.
- Which technique is safest and most effective for the given situation.
In: Identified problematic dependency · Out: Chosen dependency-breaking technique
ch10 · ch26p01 · ch26p02
Step 3Apply the technique to introduce a 'seam' where behavior can be altered for testing.
Entry: A technique has been chosen.
Exit: The production code is modified to allow for dependency substitution.
In: Chosen dependency-breaking technique · Out: Refactored code with a seam
ch09p02 · ch10 · ch12
Step 4Write a test that uses the new seam to inject a test double.
Entry: A seam has been introduced into the code.
Exit: A test successfully uses the seam to isolate the code under test.
In: Refactored code with a seam · Out: A passing unit test
ch10 · ch12
Using Test Doubles (Mocks, Fakes, Stubs)
To isolate a unit of code for testing by replacing its real dependencies with simplified, controllable substitutes.
When to use: During unit testing, after a seam has been introduced to allow for dependency substitution.
Step 1Identify the dependency that needs to be replaced with a test double.
Entry: The code under test has a dependency that complicates testing.
Exit: The dependency to be faked or mocked is identified.
- Choosing which dependencies to replace.
In: Code under test · Out: Identified dependency
ch05 · ch09p03
Step 2Create a test double (e.g., a fake, stub, or mock object) that simulates the real dependency.
Entry: The dependency is identified.
Exit: A test double implementation exists.
- Whether to hand-code a fake or use a mocking framework.
In: Interface or signature of the real dependency · Out: Test double
ch05 · ch09p01 · ch20
Step 3Configure the test double to provide the necessary behavior for the test case.
Entry: A test double has been created.
Exit: The test double is configured for a specific test scenario.
In: Test double · Out: Configured test double
ch05
Step 4Inject the configured test double into the code under test via a seam.
Entry: The test double is configured.
Exit: The code under test is using the test double.
In: Configured test double
ch05 · ch09p03
Step 5Execute the test and verify the outcome.
Entry: The test double has been injected.
Exit: The test passes, verifying the unit's behavior in isolation.
Out: Isolated unit test result
ch05
Strategic Testing for Refactoring
To establish an effective testing strategy for making changes to legacy code by identifying the best places to write tests that cover the impact of those changes.
When to use: Before starting a significant refactoring effort, to plan where to add tests for maximum safety and efficiency.
Step 1Identify the 'change points' where modifications will be made.
Entry: A refactoring goal has been defined.
Exit: A list of change points is created.
In: Refactoring goal · Out: List of change points
ch16
Step 2Trace the effects of the changes outward to find potential 'interception points'.
Entry: Change points are identified.
Exit: A map or list of potential interception points is created.
In: List of change points · Out: List of potential interception points
ch16
Step 3Identify 'pinch points' where the effects of multiple change points converge.
Entry: Effects have been traced from change points.
Exit: Strategic pinch points for testing are identified.
- Choosing the most effective pinch point that balances coverage and complexity.
In: Effect map · Out: Identified pinch points
ch16
Step 4Write higher-level 'covering tests' at the identified pinch points or interception points.
Entry: Pinch points or interception points are identified.
Exit: A suite of covering tests is in place and passing.
In: Identified pinch points · Out: Covering tests
ch15 · ch16
Step 5Perform the refactoring under the protection of the covering tests.
Entry: Covering tests are passing.
Exit: The refactoring is complete and all covering tests still pass.
In: Covering tests · Out: Refactored code
ch15
Safe Refactoring Practices
To improve the internal structure of existing code without changing its external behavior, making it easier to understand, maintain, and extend.
When to use: After establishing a test harness and characterization tests for a piece of code, before adding new features, or as part of a continuous improvement effort.
Step 1Identify a 'code smell' or area of the code that needs improvement.
Entry: The code is covered by tests.
Exit: A specific refactoring target is identified.
- Prioritizing which code smells to address first.
In: Source code · Out: Refactoring target
ch14 · ch22 · ch23
Step 2Select and apply a specific refactoring technique to address the issue.
Entry: A refactoring target is identified.
Exit: A small, incremental refactoring is applied.
- Choosing the correct refactoring pattern for the situation.
In: Refactoring target · Out: Modified code
ch14 · ch21 · ch22 · ch23
Step 3Use disciplined techniques to perform the refactoring safely.
Entry: A refactoring is being performed.
Exit: The refactoring is completed with minimal risk of error.
ch24
Step 4Run the tests to verify that behavior has not changed.
Entry: A refactoring has been applied.
Exit: All tests are passing, confirming behavior is preserved.
In: Modified code · Out: Verified refactoring
ch14 · ch22
Step 5Repeat the cycle of identifying, refactoring, and testing until the code is sufficiently improved.
Entry: The previous refactoring step was successful.
Exit: The code quality has been improved to the desired level.
Out: Cleaner, more maintainable code
ch14
Using Automated Refactoring Tools
To safely and efficiently perform common code refactorings, improving code maintainability and readability while ensuring functionality is preserved.
When to use: When performing standard refactorings like renaming, extracting methods, or changing method signatures in a supported environment.
Step 1Identify an area of code that requires a standard refactoring.
Entry: Code is covered by tests.
Exit: A target for automated refactoring is selected.
In: Source code · Out: Refactoring target
ch05 · ch09p02
Step 2Invoke the automated refactoring tool from the IDE.
Entry: A refactoring target is selected.
Exit: The refactoring tool's dialog is displayed.
- Selection of which refactoring tool/feature to use.
In: Refactoring target
ch05 · ch09p02
Step 3Review the suggestions and preview the changes provided by the tool.
Entry: The refactoring tool has analyzed the code.
Exit: The proposed changes are understood and approved.
- Determine whether the suggested changes are correct and desirable.
In: Tool-generated suggestions
ch05
Step 4Apply the refactoring.
Entry: The proposed changes have been approved.
Exit: The codebase is updated by the tool.
Out: Refactored code
ch05 · ch09p02
Step 5Run tests to confirm that existing behavior is preserved.
Entry: The automated refactoring is complete.
Exit: All tests pass, confirming the refactoring was successful.
In: Refactored code · Out: Verified refactoring
ch05 · ch09p02
Disciplined Code Editing
To reduce cognitive overload and minimize the introduction of errors during code modification by focusing on one task at a time and being highly aware of each change.
When to use: Whenever writing or modifying code.
Step 1Define a single, clear goal for the editing session.
Entry: A task needs to be performed on the code.
Exit: A single goal is established.
In: Task description · Out: Single editing goal
ch24
Step 2Focus exclusively on the single goal, resisting the urge to make unrelated changes.
Entry: A single goal is established.
Exit: The primary task is completed without unrelated side-edits.
- Recognizing when an urge to refactor arises and deciding to defer it.
Out: List of future tasks
ch24
Step 3Practice hyperaware editing by classifying each keystroke's impact.
Entry: You are actively editing code.
Exit: Code is edited with full awareness of the impact of each change.
- Determining whether a keystroke impacts code behavior.
ch24
Step 4Complete the single-goal task and verify it.
Entry: The code for the single goal has been written.
Exit: The task is complete and verified.
Out: Completed task
ch24
Step 5Address the noted secondary tasks or refactorings in separate, focused sessions.
Entry: The primary task is complete.
Exit: Secondary tasks are addressed in a focused manner.
In: List of future tasks
ch24
Test-Driven Development (TDD)
To guide the design and implementation of new functionality by writing a failing test before writing the corresponding production code, ensuring testability and correctness from the start.
When to use: When adding a new feature or fixing a bug in a testable codebase.
Step 1Write a test that defines a desired improvement or new function.
Entry: A new feature or behavior is required.
Exit: A new, specific test case is written.
In: Feature specification · Out: New test case
ch08 · ch09p02 · ch20
Step 2Run the test and watch it fail.
Entry: A new test case has been written.
Exit: The new test fails for the expected reason.
In: New test case · Out: A failing test result
ch08 · ch09p02
Step 3Write the minimal amount of production code necessary to make the test pass.
Entry: There is a single, failing test.
Exit: Production code has been written to satisfy the test.
In: Failing test result · Out: Minimal production code
ch08 · ch09p02 · ch20
Step 4Run all tests and confirm they now pass.
Entry: Minimal production code has been written.
Exit: All tests in the suite are passing.
In: Minimal production code · Out: Passing test suite
ch08
Step 5Refactor the code to improve its structure and remove duplication.
Entry: All tests are passing.
Exit: The code is clean, well-structured, and all tests still pass.
- When to refactor to improve design.
In: Working code · Out: Refactored code
ch08 · ch09p02 · ch20
Step 6Repeat the cycle for the next piece of functionality.
Entry: The previous cycle is complete.
Exit: The feature is fully implemented and tested.
ch08
Programming by Difference
To manage and minimize risks when adding new features by focusing on the specific changes (differences) between the existing code and the modified code.
When to use: When adding a feature where the primary concern is avoiding disruption to existing functionality.
Step 1Identify the existing codebase and its functionalities that will be affected.
Entry: A new feature needs to be added.
Exit: The scope of impact on the existing code is understood.
In: Current codebase, Feature documentation · Out: Analysis of affected code
ch08
Step 2Analyze and document the specific differences required for the new feature.
Entry: The impact scope is understood.
Exit: A clear plan of the required code changes is documented.
- Decide on the extent of changes required.
In: Analysis of affected code · Out: Documentation of code differences
ch08
Step 3Implement the changes while carefully monitoring interactions with existing functionalities.
Entry: The change plan is documented.
Exit: The new feature is implemented.
In: Documentation of code differences · Out: Modified codebase
ch08
Step 4Test the new feature and run regression tests to ensure existing functionality is not disrupted.
Entry: The feature is implemented.
Exit: The new feature is verified and no regressions are found.
In: Modified codebase · Out: Passing test suite
ch08
Step 5Review the implementation for clarity, stability, and adherence to the documented differences.
Entry: All tests are passing.
Exit: The new feature is successfully integrated and documented.
Out: Integrated new feature
ch08
The story
The reader A software developer—often experienced but battle-worn—who works daily in a large, poorly tested codebase and needs to add features or fix bugs without breaking things that already work.
External problem
The codebase has no tests, tightly coupled dependencies, and monster methods that make every change feel like defusing a bomb.
Internal problem
The developer feels fear, demoralization, and a creeping sense that the code is beyond rescue—and that their own competence is being undermined by the system they inherited.
Philosophical problem
It is wrong that skilled developers should be paralyzed by code they did not write; software should be improvable, not just survivable.
The plan
- Reframe the problem: recognize that any code without tests is legacy code and that the path forward is the same regardless of language or age.
- Learn to see seams—preprocessing, link, and object seams—as the leverage points already present in every program.
- Use the legacy code change algorithm: identify change points, find test points, break dependencies conservatively, write characterization tests, then make changes.
- Apply targeted dependency-breaking techniques (Extract Interface, Parameterize Constructor, Subclass and Override Method, etc.) to get specific classes and methods under test.
- Write characterization tests to pin down existing behavior before touching it.
- Add new features using Sprout Method, Sprout Class, Wrap Method, or TDD once a safety net exists.
- Incrementally decompose big classes and monster methods using feature sketches, effect sketches, and systematic extraction.
- Build islands of well-tested code and expand them deliberately over time.
Success
- Changes can be made quickly and with confidence because tests catch regressions immediately.
- Developers feel in control of their codebase rather than afraid of it.
- The codebase gradually improves with each iteration rather than rotting further.
- New features can be added cleanly without creating new legacy code.
- The team shares a common vocabulary and set of practices for dealing with difficult code.
- Programming becomes enjoyable again.
At stake
- Fear of change compounds over time, making even trivial modifications feel dangerous.
- The codebase continues to rot until it becomes unmaintainable and must be rewritten—often unsuccessfully.
- Developers leave the team or the profession due to demoralization.
- Bug counts rise as untested changes accumulate subtle regressions.
- The organization loses competitive agility because software change is too slow and risky.
Chapter by chapter
ch01Changing Software
The urgency for organizations to adapt and change their software systems is underscored by four critical reasons that highlight both the risks of inaction and the potential benefits of embracing new technologies.
ch02Working with Feedback
This chapter explores the critical role of feedback during software development, dissecting how effective feedback mechanisms can enhance productivity and create a culture of continuous improvement.
ch03Sensing and Separation
This chapter tackles the tension between the perception of collaboration in organizations and the underlying dissonance that can arise when collaborators lack true alignment or commitment.
ch04The Seam Model
The Seam Model delineates the complexities of organizational interactions by categorizing various types of seams that affect collaboration and communication, ultimately arguing for a structured approach to managing these seams for improved effectiveness.
ch05Tools
This chapter delves into essential automated tools and methodologies that enable efficient code refactoring and testing in software development, making the case for their indispensable role in enhancing software quality.
ch06I Don’t Have Much Time and I Have to Change It
The chapter argues that effective change management is not only crucial for organizational success but must also be achievable within tight timeframes. The author presents frameworks and methodologies designed to facilitate rapid and meaningful transformation.
ch07It Takes Forever to Make a Change
This chapter addresses the frustratingly slow process of implementing change within organizations, emphasizing the importance of understanding lag time and dependencies to facilitate smoother transitions.
- Lag time is an inevitable component of organizational change, stemming from both external obstacles and internal resistance.
- Identifying and breaking the dependencies that contribute to lag times is essential for achieving meaningful transformation.
- Engaging stakeholders transparently can mitigate resistance and align the organization around a shared vision of change.
- Small, incremental changes can create momentum and foster a positive culture towards transformation in the long run.
ch08How Do I Add a Feature?
This chapter explores the practicalities and methodologies for adding features in software development, emphasizing Test-Driven Development (TDD) and Programming by Difference as essential techniques.
ch09p01I Can’t Get This Class into a Test Harness (part 1/3)
The chapter addresses the complexities of placing legacy classes into a test harness, outlining common obstacles caused by code structure and dependencies, and proposing systematic solutions.
ch09p02I Can’t Get This Class into a Test Harness (part 2/3)
The chapter explores the importance of utilizing seams in legacy code to facilitate testing, which allows for safer modifications with minimal code changes.
- Properly utilizing seams can radically simplify the process of implementing tests within legacy systems.
- Object seams offer the best framework for enhancing test maintainability while minimizing disruption to existing code.
- Automated refactoring tools can significantly enhance productivity if used safely and judiciously, preserving existing behavior throughout changes.
- Engaging deeply with legacy code allows developers to demystify its complexities, ultimately leading to clearer paths for efficient changes.
ch09p03I Can’t Get This Class into a Test Harness (part 3/3)
In software development, creating a test harness for legacy classes can often be impeded by hidden dependencies and challenging constructors, which necessitates the use of strategic refactoring techniques to ensure effective testing.
ch10I Can’t Get This Class into a Test Harness
This chapter explores the fundamental challenges of testing classes with hidden dependencies, particularly focusing on handling and refactoring dependencies that inhibit effective automation in testing.
ch11My Application Has No Structure
The chapter examines the complexities of C++'s include dependencies, illustrating how legacy code can create significant challenges for testing and maintenance.
ch12This Class Is Too Big and I Don’t Want It to Get Any Bigger
When striving to maintain a single class amidst encroaching complexity, developers face the dual challenge of managing functionality while ensuring testability—often requiring strategic adjustments without full refactoring.
ch13Dependencies on Libraries Are Killing Me
This chapter explores how reliance on external libraries in software development can introduce significant risks and challenges, particularly around security and maintainability, urging developers to critically evaluate their dependencies.
- Libraries can accelerate development but may introduce significant security vulnerabilities if not carefully vetted.
- Regularly auditing and reviewing external library dependencies is critical for maintaining software integrity.
- An active community and ongoing maintenance are vital indicators of a library’s reliability and trustworthiness.
- Developers must balance the need for rapid development with rigorous scrutiny of their tools and dependencies.
ch14My Application Is All API Calls
The chapter argues for the necessity of refactoring complex classes with undetectable side effects, illustrating how to transition from UI-integrated methods to more testable code architectures.
- Complex interactions in legacy systems often lead to undetectable side effects that hinder testing and maintenance.
- Applying Extract Method refactorings separates GUI logic from business logic, driving clarity in code structure.
- Adhering to the Command/Query Separation principle allows developers to understand method behaviors better, simplifying future modifications.
- Continuous refactoring is necessary to keep systems maintainable as complexity naturally increases in application development.
ch15I Need to Make Many Changes in One Area. Do I Have to Break Dependencies for All the Classes Involved?
This chapter addresses the challenge of modifying multiple interconnected classes within legacy code while minimizing the disruption of breaking dependencies, advocating for a strategic approach to testing.
- The process of making changes to legacy code can often feel overwhelming due to numerous dependencies that need to be addressed systematically.
- Testing 'one level back' allows developers to consolidate their efforts and validate multiple changes simultaneously.
- Establishing covering tests not only safeguards current functionality but also grants latitude for future refactorings in the codebase.
- Embracing a pragmatic view of legacy code management serves to increase flexibility and reduce the tension associated with updating legacy systems.
ch16I Need to Make a Change. What Methods Should I Test?
This chapter elucidates the concept of interception points and pinch points in code testing, providing strategies for effectively locating and utilizing these points to facilitate software changes.
ch17I Need to Make a Change, but I Don’t Know What Tests to Write
This chapter argues that when modifying legacy code, rather than focusing solely on bug detection through automated tests, developers should utilize characterization tests to accurately document existing behaviors and safeguard against future errors.
- Characterization tests are not merely tools for catching bugs; they fundamentally document the actual behavior of your systems.
- Emphasizing understanding over anticipation transforms the developer's approach to legacy code, setting the stage for more sustainable modifications.
- In inadequate systems, the revival of user confidence begins with documenting expected behavior through tests, allowing for guided changes.
- An artificial distinction between tests for bug-finding and tests for behavior preservation contributes to a cycle of uncertainty in legacy environments.
ch18I Don’t Understand the Code Well Enough to Change It
Stepping into legacy code can evoke fear and uncertainty among developers; understanding how to navigate complex codebases effectively is essential to ensuring successful feature implementations without being overwhelmed.
- Stepping into legacy code can provoke significant anxiety; understanding the system requires practical techniques to overcome mental barriers.
- Sketching and note-taking are low-tech yet effective methods to grasp relationships and maintain clarity in understanding complex systems.
- Listing markup of code can help clarify roles and responsibilities, improving both individual comprehension and team communication.
- Scratch refactoring allows developers to explore and learn about code without the pressure of producing final results, fostering a safer learning environment.
ch19My Test Code Is in the Way
This chapter addresses the common frustration of managing test code alongside production code, emphasizing the significance of organization and naming conventions in streamlining the testing process.
- Establishing a clear naming convention for unit tests is essential for maintaining organization in your codebase.
- Keeping test and production codes in the same directory can greatly improve productivity and navigability.
- The prefix and suffix strategies for naming provide clarity and enhance the structure of your project.
- Avoid separating tests from production code without a well-justified need, as this can inhibit efficient collaboration and productivity.
ch20My Project Is Not Object Oriented. How Do I Make Safe Changes?
This chapter examines strategies for making safe changes to procedural codebases that lack object-oriented structures, emphasizing techniques like link seams, macro preprocessors, and function pointers.
ch21I Can’t Run This Method in a Test Harness
This chapter explores how to refactor code safely when tests cannot be established, emphasizing a methodical approach to extracting classes while mitigating risks associated with inheritance and variable shadowing.
ch22I’m Changing the Same Code All Over the Place
This chapter dissects the pervasive issue of code duplication within legacy systems and outlines a systematic approach to refactor and reduce redundancy, enhancing overall system efficiency.
ch23I Need to Change a Monster Method and I Can’t Write Tests for It
Refactoring monster methods in legacy code presents unique challenges, particularly when robust testing is absent; this chapter offers practical strategies for overcoming these difficulties.
- Monster methods pose unique challenges during refactoring and can stifle code maintainability when left unaddressed.
- Automated refactoring tools are useful but must be employed with caution due to their limitations.
- Introducing sensing variables helps developers understand code dependencies and eases the testing of complex methods.
- Small, confident extractions are crucial for successful refactoring of monster methods.
ch24How Do I Know That I’m Not Breaking Anything?
This chapter confronts the risks inherent to software editing and offers numerous strategies to mitigate the potential for introducing errors, focusing on practices that enhance awareness and discipline during code modifications.
- Code is inherently fragile; unlike physical materials, it does not exhibit gradual breakdown but can fail dramatically due to minor edits.
- Hyperaware editing and test-driven development cultivate a deeper understanding of the implications of your changes, reducing error rates.
- The mantra 'programming is the art of doing one thing at a time' serves as a valuable reminder to avoid fragmentation of focus during coding tasks.
- Preserving function signatures during edits is a crucial strategy to minimize the risk of errors when refactoring.
ch25We Feel Overwhelmed. It Isn’t Going to Get Any Better
This chapter argues that feeling overwhelmed while working with legacy code is a common experience and that instead of expecting conditions to improve, programmers must proactively find personal and communal motivations to thrive in such environments.
- Working with legacy code is inevitably challenging, but one’s attitude and engagement with the task can dramatically alter the experience.
- Finding community and camaraderie among programmers can transform overwhelming environments into shared journeys of improvement.
- The myth that green-field development is significantly easier is dispelled; legacy code often requires as much attention and passion as new projects.
- In the face of software challenges, fostering small victories within the codebase can ignite enthusiasm and motivation.
ch26p01Dependency-Breaking Techniques (part 1/2)
This chapter introduces techniques for breaking dependencies in code to facilitate testing and improve maintainability, emphasizing non-intrusive refactorings that allow for behavior preservation.
ch26p02Dependency-Breaking Techniques (part 2/2)
This chapter discusses advanced techniques to break dependencies in software design, focusing on key methodologies such as Pull Up Feature, Push Down Dependency, and Replace Function with Function Pointer.
Questions this book answers
- What is legacy code and why is it dangerous to change?
- How can I change code safely when there are no tests?
- How do I get an untestable class into a test harness?
- Where should I write tests when I need to make a change?
- How do I break dependencies without introducing new bugs?
Related in the library
- Architecture Patterns with Pythonshared: Systems
- Clean Architecture A Craftsmans Guide to Software Structure and Design (Robert C. Martin Series)shared: Systems
- A Philosophy of Software Design (2nd Edition)shared: Systems
- Accelerate The Science of DevOpsshared: Systems
- Analytics Engineering with SQL and dbt Building Meaningful Data Models at Scaleshared: Systems
- Artificial Intelligence - A Very Short Introductionshared: Systems
Tools these methods power