Blog Archives

Working Effectively with Legacy Code by Michael Feathers – Book Review:

Introduction

“Working Effectively with Legacy Code” by Michael Feathers is a book that addresses the challenges of working with large, complex, and poorly designed codebases. The book aims to provide practical guidance and techniques for software developers who must maintain, refactor, or extend existing code.

The book is divided into three parts. The first part introduces the concept of legacy code and explains why it is difficult to work with. It defines legacy code as code that lacks unit tests, is hard to change, and often contains subtle bugs. The author explains how legacy code can be a result of various factors such as outdated technologies, poor architecture, and lack of design.

The second part of the book describes techniques for working with legacy code. It introduces the concept of “characterization tests” that can help developers to understand the behavior of the codebase. The author then discusses techniques such as “seam identification”, “dependency breaking”, and “test-driving changes” that can help to refactor and improve legacy code without breaking its existing behavior.

The third part of the book discusses strategies for working with legacy code in a team setting. It provides guidance on how to deal with issues such as team resistance, code ownership, and change management. The author emphasizes the importance of communication and collaboration among team members to effectively work with legacy code.

Throughout the book, the author provides many real-world examples and code snippets to illustrate the techniques and concepts presented. The book also includes a comprehensive set of “code smells” and “dependency-breaking techniques” that can help developers to identify and address issues in legacy code.

Overall, “Working Effectively with Legacy Code” is a valuable resource for software developers who must work with legacy codebases. It provides practical techniques and strategies for dealing with the challenges of working with legacy code and can help developers to improve the quality and maintainability of existing code.

Part 1

Part one of “Working Effectively with Legacy Code” sets the stage for the rest of the book by defining what legacy code is, why it is difficult to work with, and what makes it valuable.

The author begins by defining legacy code as any code that lacks automated tests, which makes it difficult to refactor or extend safely. He explains that legacy code often accumulates over time due to changing requirements, evolving technologies, and shifting priorities, and may not have been designed with maintainability or testability in mind.

The author then discusses why working with legacy code is difficult. He identifies several characteristics of legacy code that make it challenging, including its size and complexity, its lack of structure and modularity, and its tight coupling and interdependence. He explains that legacy code often contains “code smells” such as long methods, global variables, and deep inheritance hierarchies, which make it difficult to understand and change.

Despite its challenges, the author argues that legacy code is valuable and worth investing in. He explains that legacy code represents a significant investment of time and resources, and often contains valuable business logic and domain knowledge. He suggests that developers should approach legacy code as an opportunity to learn and improve their skills, rather than simply as a burden to be borne.

Part one of the book concludes by introducing the concept of “characterization tests”, which are a form of automated tests that can help developers to understand the behavior of legacy code. The author argues that characterization tests are a crucial first step in working with legacy code, as they provide a safety net for making changes and refactoring the codebase. He suggests that developers should focus on creating characterization tests before attempting to refactor or extend legacy code, as this will help them to gain a better understanding of the codebase and reduce the risk of introducing bugs.

Here are some examples from Part One of “Working Effectively with Legacy Code”:

  1. Legacy Code Example:
    • In Chapter 1, the author provides an example of a legacy codebase that has grown over time without a clear design or architecture. He notes that the codebase contains many interdependent modules and global variables, which make it difficult to understand and change. For example, imagine a legacy codebase for a financial application that has been in use for several years. The codebase contains multiple modules that are tightly coupled, and many global variables are used to pass information between modules. This makes it difficult to understand the behavior of the application and make changes safely.
  1. Code Smell Example:
    • In Chapter 2, the author provides an example of a code smell – long methods – that is common in legacy code. He notes that long methods make it difficult to understand the behavior of the code and make changes safely. For example, imagine a legacy codebase that contains a method for processing customer orders that is over 500 lines long. This method contains many nested if statements and loops, making it difficult to understand the behavior of the code and make changes safely.
  1. Characterization Test Example:
    • In Chapter 3, the author provides an example of a characterization test for a legacy codebase. He notes that characterization tests are a form of automated tests that are not concerned with the correctness of the code, but rather with its behavior. For example, imagine a legacy codebase for a web application that contains a complex search algorithm. A characterization test for this algorithm might simulate user input and verify that the output is as expected, without worrying about the implementation details.
  1. Seam Example:
    • In Chapter 4, the author provides an example of a seam – a place in the code where behavior can be changed without affecting the rest of the codebase. He notes that seams can be used to make changes to legacy code safely. For example, imagine a legacy codebase for a game that contains a method for calculating the score. The method takes a single argument – the current score – and returns the new score. A seam can be introduced by passing in a function that calculates the new score, allowing the behavior to be changed without affecting the rest of the codebase.
  1. Dependency-Breaking Technique Example:
    • In Chapter 5, the author provides an example of a dependency-breaking technique – extracting interfaces – that can be used to make legacy code more modular and testable. He notes that dependencies between modules are a major source of complexity in legacy code and can make it difficult to understand and change. For example, imagine a legacy codebase for a video streaming application that contains a module for handling user authentication. By extracting an interface for the authentication module, other modules can use the interface instead of directly accessing the implementation, making it easier to change the behavior of the module and test it independently.

Part 2

Part Two of the book focuses on techniques for improving the safety and maintainability of legacy code. The author emphasizes that the key to working with legacy code is to be able to make changes safely and that this requires a combination of understanding the behavior of the code and having effective automated tests.

The chapters in Part Two cover a range of topics related to improving the safety and maintainability of legacy code. Here is an overview of some of the key concepts covered in each chapter:

  1. I Don’t Have Much Time and I Have to Change It:
    • This chapter focuses on techniques for making changes to legacy code quickly and safely. The author emphasizes the importance of understanding the behavior of the code before making changes and provides examples of techniques for making changes safely, such as using temporary variables to isolate code changes and using a debugger to understand the behavior of the code.
      • Example: A legacy codebase has a function that takes a long time to execute due to inefficient algorithms. The developer needs to make changes to improve the performance but also needs to ensure that the behavior of the function remains the same. To do this safely, the developer can use a profiler to identify the slowest parts of the function and make changes to those areas, using temporary variables to isolate the changes and a debugger to test the behavior.
  1. It Takes Forever to Make a Change:
    • This chapter focuses on techniques for reducing the time it takes to make changes to legacy code. The author emphasizes the importance of understanding the dependencies between modules and minimizing them where possible. He also provides examples of techniques for making changes safely, such as using conditional compilation and extracting methods.
      • Example: A legacy codebase has a complex set of interdependent modules, making it difficult to make changes without affecting other parts of the system. To reduce the time it takes to make changes, the developer can use techniques such as refactoring to reduce the complexity of the code and identify the dependencies between modules to minimize them where possible. They can also use techniques such as conditional compilation to isolate changes and extract methods to make the code more modular.
  1. How Do I Add a Feature?:
    • This chapter focuses on techniques for adding new features to legacy code. The author emphasizes the importance of understanding the existing behavior of the code and identifying the seams where new behavior can be added safely. He also provides examples of techniques for adding new features safely, such as using a characterization test to understand the behavior of the code and introducing a new module to handle the new feature.
      • Example: A legacy codebase does not have the ability to handle a new type of input data. The developer needs to add support for this new data type but needs to ensure that the existing behavior of the system remains unchanged. To add this feature safely, the developer can use techniques such as a characterization test to understand the existing behavior of the code, identify the seams where new behavior can be added safely, and introduce a new module to handle the new feature.
  1. I Can’t Get This Class into a Test Harness:
    • This chapter focuses on techniques for testing legacy code that was not designed with testing in mind. The author emphasizes the importance of identifying the seams where behavior can be changed and using them to introduce testability into the code. He also provides examples of techniques for testing legacy code, such as using a driver module to simulate input and output and introducing a test-specific subclass to override behavior.
      • Example: A legacy codebase has a class that was not designed with testing in mind, making it difficult to write automated tests for it. To make this class testable, the developer can identify the seams where the behavior of the class can be changed, such as methods that take input parameters or return values, and use those seams to introduce testability. They can also use techniques such as a driver module to simulate input and output and introduce a test-specific subclass to override behavior.
  1. I’m Changing the Same Code All Over the Place:
    • This chapter focuses on techniques for reducing the amount of duplicate code in a legacy codebase. The author emphasizes the importance of identifying the common behavior that is duplicated and extracting it into a single module or method. He also provides examples of techniques for reducing duplicate code, such as using a template method pattern to encapsulate common behavior and using a superclass to encapsulate common behavior across multiple subclasses.
      • Example: A legacy codebase has a lot of duplicate code, making it difficult to maintain and change. To reduce the amount of duplicate code, the developer can identify the common behavior that is duplicated and extract it into a single module or method. They can also use techniques such as a template method pattern to encapsulate common behavior and a superclass to encapsulate common behavior across multiple subclasses.

These examples demonstrate how the techniques outlined in Part Two of the book can be applied to real-world scenarios in legacy codebases. By using these techniques, developers can make changes to legacy code safely and with confidence, improving the maintainability and sustainability of the codebase over time.

Part 3

Part Three of the book focuses on specific techniques and tools that can be used to work effectively with legacy code. Each chapter provides an overview of a particular technique or tool, along with examples of how it can be used to improve the quality and maintainability of legacy code.

  1. The Seam Model:
  • This chapter introduces the Seam Model, which is a way to identify and introduce seams into legacy code that can be used to change the behavior of the code safely. Seams are places in the code where behavior can be changed without affecting the rest of the system and can include methods that take input parameters or return values, global variables, and system calls.
  • In the Seam Model chapter, the author provides an example of a legacy codebase that sends email notifications and shows how to introduce a seam in the code to change the behavior of the email-sending function. The seam is introduced by modifying the function to take a parameter that specifies the email-sending method to use and then calling the function with different parameters depending on the desired behavior.
  1. Tools:
  • This chapter discusses a variety of tools that can be used to work effectively with legacy code, including refactoring tools, automated testing tools, profiling tools, and build tools. These tools can help developers to automate repetitive tasks, identify areas of the code that need improvement, and improve the overall quality of the codebase.
  • The Tools chapter provides examples of various tools that can be used to work effectively with legacy code. For example, the author discusses how to use refactoring tools to extract code into smaller, more manageable pieces, and how to use automated testing tools to ensure that changes to the codebase do not introduce regressions. The chapter also provides guidance on how to choose the right tools for a particular project, and how to integrate tools into an existing development workflow.
  1. Cross-Cutting Concerns:
  • This chapter focuses on cross-cutting concerns, which are aspects of a system that affect multiple parts of the codebase. Examples of cross-cutting concerns include logging, error handling, and security. To address cross-cutting concerns in legacy code, the chapter recommends using techniques such as aspect-oriented programming and dependency injection.
  • In the Cross-Cutting Concerns chapter, the author provides an example of a legacy codebase that lacks proper error handling and shows how to introduce error handling using aspect-oriented programming (AOP). AOP allows developers to separate cross-cutting concerns such as error handling from the core logic of the code, making it easier to maintain and modify the codebase over time.
  1. Putting It All Together:
  • This chapter provides an overview of how to use the techniques and tools discussed in the previous chapters to work effectively with legacy code. The chapter outlines a process for working with legacy code that includes identifying the parts of the code that need improvement, introducing seams to make changes safely, and using tools to automate repetitive tasks and improve code quality.
  • The Putting It All Together chapter provides an example of a legacy codebase that is difficult to modify due to tight coupling between modules. The chapter shows how to use the techniques and tools discussed in earlier chapters to identify the areas of the codebase that need improvement, introduce seams to safely modify the code, and refactor the code to reduce complexity and improve maintainability.
  1. My Application Has No Structure:
  • This chapter discusses the challenges of working with legacy code that lacks structure or architecture and provides recommendations for introducing structure into such code. Techniques discussed include using a layered architecture, introducing design patterns such as the factory pattern and the template method pattern, and using refactoring to extract modules and reduce complexity.
  • The My Application Has No Structure chapter provides an example of a legacy codebase that lacks a clear architecture or structure, making it difficult to understand and modify. The chapter shows how to introduce a layered architecture to the codebase, and provides guidance on how to use design patterns such as the factory pattern and the template method pattern to reduce coupling and improve maintainability.

Overall, Part Three of “Working Effectively with Legacy Code” provides a wealth of practical advice and examples on how to work effectively with legacy code. The techniques and tools discussed in this part of the book can be applied to a wide range of legacy codebases and can help developers to improve the quality, maintainability, and agility of their software systems.

Final words:

The final words of “Working Effectively with Legacy Code” encourage readers to embrace the challenges and rewards of working with legacy code, and to use the techniques and tools presented in the book to make positive changes in their codebases. The author emphasizes that although working with legacy code can be difficult and frustrating at times, it is also an opportunity to learn and grow as a developer, and to make a real difference in the quality and maintainability of software systems.

The book concludes with the following words: “Working with legacy code can be tough, but it’s also an opportunity to make a real difference. Use the techniques and tools presented in this book to take control of your codebase, and to make it a source of pride and satisfaction. With patience, persistence, and a willingness to learn, you can transform even the most challenging legacy code into a well-designed, maintainable, and responsive software system.”