This chapter talks about ways to work around legacy code when you don't have time to refactor first. This happens a lot because it's hard to predict how long adding a feature to legacy code will take, and it's tempting to hack first and leave refactoring till the end (i.e. never).
Before using these techniques, see if you can get the legacy code into a test harness - it may be easier than you think.
If not, these techniques will reduce the risk and/or avoid making the legacy code incrementally worse over time.
The downside compared to refactoring the legacy code is that the legacy code won't go away, and the codebase will accumulate non-legacy bits that duplicate concepts from the legacy code. This is not so bad though, as it encourages further refactoring later as you find yourself reading more tested code.
Changes tend to cluster so if you are working in a particular area today chances are you will need to look at it again soon.
Changes are invasive if they mingle different operations and/or introduce new temporary variables into existing methods
To minimise the invasiveness, first study the existing code and then sprout methods:
- Figure out where in the source method the change could be made as a sequence of statements
- Make any local variables it depends on parameters of a new method
- Determine return value the source method will need
- TDD the new method
- Uncomment the call
Disadvantages:
- may introduce temporal coupling i.e. things that are together because they need to be done at the same time, but the relationship between them isn't strong
This is similar to sprout method.
May be motivated by introducing a new responsibility that shouldn't be part of the original, or you can't get the original under test.
Sprouted methods may become part of the original concept (if you get it under test) or might not. You may extract common code as the design evolves
Advantages:
- can get on with the change you want to make with more confidence
Disadvantages:
- increases conceptual complexity
- things that logically should belong in one class are split up
- Create method with the same name as the old method
- Delegate to the old code
- Add a new method
Or, just add a new wrapper method if nothing calls it yet.
Advantages:
- Decouples the two behaviours
Disadvantages:
- You have to make up a new name, which might make less sense than the old one
Use when the behaviour is entirely independent of the class you have to change, or too low level
Or if you have reached the point where you want to make no more changes to a legacy class at all (in this case you should have a roadmap for later changes)
This pattern allows you to compose objects at runtime
- Extract an interface from the original implemntation
- Have the new implementation implement that interface
- The new implementation can accept another implementor as a parameter
- New implementation delegates to old implementation
Advantages:
- you can test the new subclass
Disadvantages:
- hard to navigate/debug if there are lots of layers
This is useful if there is only one place the old code is being called
- Create a new class that wraps the old class
- Use TDD to create methods on the new class (delegating to the old class as apprioriate)
- Update the call site to use the new class