Refactoring Techniques
Refactoring is the process of restructuring existing computer code—changing the factoring—without changing its external behavior. It is essentially “cleaning up” code to make it more maintainable and understandable.
Why Refactor?
- Improve Design: Code drifts over time as new features are bolted on. Refactoring brings it back to a cohesive design.
- Reduced Complexity: Simplifies logic and removes duplication.
- Easier Debugging: Cleaner code makes bugs easier to spot.
- Faster Development: Well-structured code is easier to build upon.
When to Refactor?
- Rule of Three: When you’re doing something for the third time, refactor.
- When adding a feature: Clean up the area you’re about to modify to make the new feature easier to implement.
- When fixing a bug: If code is so messy that a bug was hard to find, it’s a sign it needs refactoring.
- During Code Review: Use reviews as an opportunity to suggest refactoring.
Common Code Smells
A “code smell” is a surface indication that usually corresponds to a deeper problem in the system.
- Bloaters: Large classes or methods (e.g., Long Method, Large Class, Data Clumps).
- Object-Orientation Abusers: Improper use of OO principles (e.g., Switch Statements where polymorphism should be used, Alternative Classes with Different Interfaces).
- Change Preventers: Code that is hard to change (e.g., Divergent Change, Shotgun Surgery).
- Dispensables: Code that should be removed (e.g., Comments, Duplicate Code, Dead Code).
- Couplers: Excessive coupling between classes (e.g., Feature Envy, Inappropriate Intimacy).
Core Refactoring Techniques
1. Extract Method
If you have a code fragment that can be grouped together, turn the fragment into a method whose name explains the purpose of the method.
Before:
void printOwing() {
printBanner();
// Print details
System.out.println("name: " + name);
System.out.println("amount: " + getOutstanding());
}
After:
void printOwing() {
printBanner();
printDetails(getOutstanding());
}
void printDetails(double outstanding) {
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}
2. Replace Conditional with Polymorphism
If you have a conditional (switch or if-else) that performs different behaviors depending on the type of an object, use subclasses and polymorphism instead.
Before:
def get_speed(bird):
if bird.type == "EUROPEAN":
return base_speed()
elif bird.type == "AFRICAN":
return base_speed() - load_factor() * bird.num_coconuts
elif bird.type == "NORWEGIAN_BLUE":
return 0 if bird.is_nailed else base_speed()
After:
class Bird:
def get_speed(self): pass
class European(Bird):
def get_speed(self): return base_speed()
class African(Bird):
def get_speed(self): return base_speed() - load_factor() * self.num_coconuts
class NorwegianBlue(Bird):
def get_speed(self): return 0 if self.is_nailed else base_speed()
3. Move Method / Field
If a method is used more by another class than the one it’s in, move it to the other class. This reduces Feature Envy.
4. Replace Temp with Query
Replace a temporary variable used to store an expression with a method. This makes the logic reusable throughout the class.
Before:
let basePrice = quantity * itemPrice;
if (basePrice > 1000) return basePrice * 0.95;
else return basePrice * 0.98;
After:
if (basePrice() > 1000) return basePrice() * 0.95;
else return basePrice() * 0.98;
function basePrice() {
return quantity * itemPrice;
}
The Workflow of Refactoring
- Ensure Tests are Green: Never refactor without a solid test suite.
- Make Small Changes: One refactoring at a time.
- Run Tests: After every small change, run the tests to ensure you haven’t broken the external behavior.
- Commit Often: This allows you to revert easily if a refactor goes wrong.