Key Takeaways
1. Refactoring: Controlled Code Improvement
Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure.
Definition and Purpose. Refactoring is not about adding new features or fixing bugs; it's about improving the internal design of existing code without changing its external behavior. It's a disciplined approach to cleaning up code, making it easier to understand, modify, and maintain. The goal is to prevent software decay and ensure that the code remains robust and adaptable over time.
Two Hats Metaphor. When refactoring, you should wear two distinct hats: one for adding functionality and another for refactoring. When adding functionality, you focus on new features and tests. When refactoring, you focus on restructuring the code without adding new features or tests. This separation of concerns helps maintain focus and prevents the introduction of bugs.
Continuous Process. Refactoring is not a one-time event but an ongoing process that should be integrated into the development cycle. It's a continuous effort to improve the design of the code as you work on it, rather than a separate activity done periodically. This approach ensures that the code remains clean and maintainable throughout its lifecycle.
2. Code Smells: Identifying Refactoring Needs
If it stinks, change it.
Indicators of Poor Code. Code smells are indicators of potential problems in the code that suggest the need for refactoring. These are not necessarily bugs, but they are signs of poor design that can lead to future problems. Common code smells include:
- Duplicated Code: Identical or similar code in multiple places
- Long Method: Methods that are too long and complex
- Large Class: Classes that have too many responsibilities
- Long Parameter List: Methods with too many parameters
- Divergent Change: Classes that are changed for different reasons
- Shotgun Surgery: Changes that require modifications in many classes
- Feature Envy: Methods that are more interested in another class's data
- Data Clumps: Groups of data that appear together in multiple places
- Primitive Obsession: Overuse of primitive data types instead of objects
- Switch Statements: Conditional logic based on type codes
- Parallel Inheritance Hierarchies: Subclasses that mirror each other
- Lazy Class: Classes that don't do enough to justify their existence
- Speculative Generality: Unnecessary complexity for future needs
- Temporary Field: Fields that are used only in certain circumstances
- Message Chains: Long sequences of method calls
- Middle Man: Classes that delegate most of their work
- Inappropriate Intimacy: Classes that are too coupled
- Alternative Classes with Different Interfaces: Classes that do the same thing with different interfaces
- Incomplete Library Class: Library classes that lack needed functionality
- Data Class: Classes that only hold data
- Refused Bequest: Subclasses that don't use inherited methods
- Comments: Excessive comments that indicate unclear code
Subjective but Useful. Code smells are subjective, but they provide a practical way to identify areas of code that need attention. They are not precise rules but rather guidelines that help you focus on areas that may benefit from refactoring.
Actionable Insights. Code smells are not just about identifying problems; they also suggest specific refactorings that can be used to address those problems. By recognizing the smells, you can choose the appropriate refactoring techniques to improve the code.
3. Testing: The Bedrock of Safe Refactoring
Before you start refactoring, check that you have a solid suite of tests. These tests must be self-checking.
Essential for Refactoring. Tests are crucial for refactoring because they provide a safety net that allows you to make changes with confidence. Without tests, you risk introducing bugs and breaking existing functionality. Tests should be self-checking, meaning they automatically verify the results and report any failures.
Self-Checking Tests. Self-checking tests are essential for efficient refactoring. They eliminate the need for manual verification, allowing you to run tests frequently and quickly. This rapid feedback loop is crucial for identifying and fixing bugs early in the refactoring process.
Test-Driven Development. Writing tests before you write code is a powerful technique that can improve the design and quality of your software. By writing tests first, you clarify your requirements and ensure that your code meets those requirements. This approach also makes refactoring easier because you have a solid set of tests to rely on.
4. Method Composition: Breaking Down Complexity
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
Small Methods are Key. Short, well-named methods are easier to understand, reuse, and modify. They also make it easier to see the overall structure of the code. Long methods, on the other hand, are difficult to understand and maintain.
Extract Method. The most common refactoring for method composition is Extract Method, which involves breaking down a long method into smaller, more manageable pieces. This technique improves readability and makes it easier to move code around.
Other Composition Techniques. Other refactorings for method composition include:
- Inline Method: Replacing a method call with its body
- Replace Temp with Query: Replacing temporary variables with method calls
- Split Temporary Variable: Splitting a temporary variable into multiple variables
- Remove Assignments to Parameters: Avoiding assignments to parameters
- Replace Method with Method Object: Turning a method into an object
- Substitute Algorithm: Replacing an algorithm with a clearer one
By using these techniques, you can create methods that are easier to understand, reuse, and modify, leading to more maintainable and robust code.
5. Feature Movement: Placing Responsibilities Correctly
In most cases a method should be on the object whose data it uses.
Object-Oriented Principles. Object-oriented programming is about packaging data with the behavior that operates on that data. Methods should be placed on the object that owns the data they use. This principle helps to create a more cohesive and understandable design.
Move Method. The Move Method refactoring involves moving a method from one class to another. This is often done when a method is more interested in the data of another class than the class it is currently in.
Move Field. The Move Field refactoring involves moving a field from one class to another. This is often done when a field is more closely related to the behavior of another class.
Other Feature Movement Techniques. Other refactorings for feature movement include:
- Extract Class: Creating a new class to encapsulate a subset of responsibilities
- Inline Class: Merging a class into another class
- Hide Delegate: Hiding the details of a delegate object
- Remove Middle Man: Removing unnecessary delegation
- Introduce Foreign Method: Adding a method to a class you can't modify
- Introduce Local Extension: Creating a new class to extend a class you can't modify
By using these techniques, you can ensure that methods and data are placed on the objects that are most appropriate for them, leading to a more cohesive and understandable design.
6. Data Organization: Structuring for Clarity
Good code should communicate what it is doing clearly, and variable names are a key to clear code.
Data as Objects. Data should be treated as objects, not just primitive types. This means encapsulating data with behavior and creating objects that represent meaningful concepts.
Refactorings for Data Organization:
- Self Encapsulate Field: Accessing fields through accessors
- Replace Data Value with Object: Turning a data value into an object
- Change Value to Reference: Turning a value object into a reference object
- Change Reference to Value: Turning a reference object into a value object
- Replace Array with Object: Replacing an array with an object
- Duplicate Observed Data: Duplicating data to keep it in sync
- Change Unidirectional Association to Bidirectional: Adding a back pointer
- Change Bidirectional Association to Unidirectional: Removing a back pointer
- Replace Magic Number with Symbolic Constant: Replacing magic numbers with constants
- Encapsulate Field: Making a field private and providing accessors
- Encapsulate Collection: Returning a read-only view of a collection
- Replace Record with Data Class: Turning a record into a data class
- Replace Type Code with Class: Replacing a type code with a class
- Replace Type Code with Subclasses: Replacing a type code with subclasses
- Replace Type Code with State/Strategy: Replacing a type code with a state object
By using these techniques, you can create data structures that are more meaningful, easier to understand, and more robust to change.
7. Conditional Simplification: Making Logic Transparent
When you find you have to add a feature to a program, and the program's code is not structured in a convenient way to add the feature, first refactor the program to make it easy to add the feature, then add the feature.
Clear Conditional Logic. Conditional logic can quickly become complex and difficult to understand. It's important to simplify conditional expressions to make them more readable and maintainable.
Refactorings for Conditional Simplification:
- Decompose Conditional: Breaking down a conditional into smaller methods
- Consolidate Conditional Expression: Combining multiple conditions into one
- Consolidate Duplicate Conditional Fragments: Removing duplicate code in conditional branches
- Remove Control Flag: Replacing control flags with break or return statements
- Replace Nested Conditional with Guard Clauses: Using guard clauses for special cases
- Replace Conditional with Polymorphism: Using polymorphism to handle conditional behavior
- Introduce Null Object: Replacing null checks with a null object
- Introduce Assertion: Making assumptions explicit with assertions
By using these techniques, you can create conditional logic that is easier to understand, modify, and maintain, leading to more robust and reliable code.
8. Interface Refinement: Streamlining Interactions
The key here is not method length but the semantic distance between what the method does and how it does it.
Clear and Concise Interfaces. Interfaces should be clear, concise, and easy to use. They should communicate the purpose of the methods and make it easy for clients to interact with the object.
Refactorings for Interface Refinement:
- Rename Method: Changing the name of a method to better reflect its purpose
- Add Parameter: Adding a parameter to a method
- Remove Parameter: Removing a parameter from a method
- Separate Query from Modifier: Separating methods that return a value from those that change state
- Parameterize Method: Combining similar methods into one with a parameter
- Replace Parameter with Explicit Methods: Creating separate methods for each parameter value
- Preserve Whole Object: Passing the whole object instead of individual values
- Replace Parameter with Method: Replacing a parameter with a method call
- Introduce Parameter Object: Replacing multiple parameters with an object
- Remove Setting Method: Removing a setting method for a field
- Hide Method: Making a method private
- Replace Constructor with Factory Method: Replacing a constructor with a factory method
- Encapsulate Downcast: Moving downcasts to within the method
- Replace Error Code with Exception: Replacing error codes with exceptions
- Replace Exception with Test: Replacing exceptions with conditional tests
By using these techniques, you can create interfaces that are easier to understand, use, and maintain, leading to more flexible and robust code.
9. Generalization: Leveraging Inheritance Effectively
Design Patterns provide targets for your refactorings.
Effective Use of Inheritance. Inheritance is a powerful tool for code reuse and polymorphism, but it should be used carefully. It's important to ensure that subclasses are truly subtypes of their superclasses and that they inherit only the behavior that is relevant to them.
Refactorings for Generalization:
- Pull Up Field: Moving a field to the superclass
- Pull Up Method: Moving a method to the superclass
- Pull Up Constructor Body: Moving constructor logic to the superclass
- Push Down Method: Moving a method to the subclass
- Push Down Field: Moving a field to the subclass
- Form Template Method: Creating a template method in the superclass
- Extract Subclass: Creating a new subclass
- Extract Superclass: Creating a new superclass
- Extract Interface: Creating a new interface
- Collapse Hierarchy: Merging a superclass and subclass
- Replace Inheritance with Delegation: Replacing inheritance with delegation
- Replace Delegation with Inheritance: Replacing delegation with inheritance
By using these techniques, you can create inheritance hierarchies that are more flexible, maintainable, and easier to understand.
10. Big Refactorings: Transforming Entire Systems
The most important lesson from this example is the rhythm of refactoring: test, small change, test, small change, test, small change. It is that rhythm that allows refactoring to move quickly and safely.
Large-Scale Changes. Big refactorings are large-scale changes that can transform entire systems. They often involve multiple refactorings and require a significant amount of time and effort.
Examples of Big Refactorings:
- Tease Apart Inheritance: Separating tangled inheritance hierarchies
- Convert Procedural Design to Objects: Turning procedural code into object-oriented code
- Separate Domain from Presentation: Separating business logic from user interface code
- Extract Hierarchy: Simplifying a complex class by creating a hierarchy
Incremental Approach. Big refactorings should be done incrementally, one step at a time. This approach allows you to make progress without breaking the system and to adapt to new insights as you go.
Team Collaboration. Big refactorings often require collaboration among the entire development team. It's important to communicate the goals of the refactoring and to ensure that everyone is on the same page.
By using these techniques, you can transform entire systems, making them more flexible, maintainable, and easier to evolve.
Last updated:
Review Summary
Refactoring is highly regarded as an essential read for software developers, offering practical techniques to improve code quality. Readers appreciate its clear explanations, JavaScript examples, and catalog of refactorings. Many find it useful as a reference guide throughout their careers. Some criticize outdated content in older editions and paper quality issues. While experienced developers may find less new information, most agree it's valuable for understanding and applying refactoring principles. The book emphasizes incremental changes, controlled processes, and adapting refactoring techniques to specific contexts.
Similar Books
Download PDF
Download EPUB
.epub
digital book format is ideal for reading ebooks on phones, tablets, and e-readers.