Refactoring differs from general cleanup in that we aren’t just doing low-risk things such as reformatting source code, or invasive and risky things such as rewriting chunks of it. Instead, we are making a series of small structural modifications, supported by tests to make the code easier to change.
In general, three different things can change when we do work in a system: structure, functionality, and resource usage.
The big deal is that we often don’t know how much of that behavior is at risk when we make our changes.
Preserving existing behavior is one of the largest challenges in software development. Even when we are changing primary features, we often have very large areas of behavior that we have to preserve.
- What changes do we have to make?
- How will we know that we’ve done them correctly?
- How will we know that we haven’t broken anything?
How much change can you afford if changes are risky?
“If it’s not broke, don’t fix it.”
When we avoid creating new classes and methods, the existing ones grow larger and harder to understand.
The difference between good systems and bad ones is that, in the good ones, you feel pretty calm after you’ve done that learning, and you are confident in the change you are about to make. In poorly structured code, the move from figuring things out to making changes feels like jumping off a cliff to avoid a tiger.
Avoiding change has other bad consequences. When people don’t make changes often they get rusty at it.
The last consequence of avoiding change is fear. Unfortunately, many teams live with incredible fear of change and it gets worse every day.
When you use Edit and Pray, you carefully plan the changes you are going to make, you make sure that you understand the code you are going to modify, and then you start to make the changes. When you’re done, you run the system to see if the change was enabled, and then you poke around further to make sure that you didn’t break anything. The poking around is essential.
But safety isn’t solely a function of care. I don’t think any of us would choose a surgeon who operated with a butter knife just because he worked with care. Effective software change, like effective surgery, really involves deeper skills. Working with care doesn’t do much for you if you don’t use the right tools and techniques.
Cover and Modify is a different way of making changes. The idea behind it is that it is possible to work with a safety net when we change software. Instead, it’s kind of like a cloak that we put over code we are working on to make sure that bad changes don’t leak out and infect the rest of our software. Covering software means covering it with tests.
When we have a good set of tests around a piece of code, we can make changes and find out very quickly whether the effects were good or bad.
We start to refactor the code a bit. We extract some methods and move some conditional logic. After every little change that we make, we run that little suite of unit tests. They pass almost every time that we run them. A few minutes ago, we made a mistake and inverted the logic on a condition, but a test failed and we recovered in about a minute. When we are done refactoring, the code is much clearer. We make the change we set out to make, and we are confident that it is right. We added some tests to verify the new behavior. The next programmers who work on this piece of code will have an easier time and will have tests that cover its functionality.
Unit testing is one of the most important components in legacy code work. System-level regression tests are great, but small, localized tests are invaluable. They can give you feedback as you develop and allow you to refactor with much more safety.
Dependency is one of the most critical problems in software development. Much legacy code work involves breaking dependencies so that change can be easier.
When we change code, we should have tests in place. To put tests in place, we often have to change code.
sometimes testing a class that uses it is easier; regardless, we usually have to break dependencies between classes someplace.
When we break dependencies, we can often write tests that make more invasive changes safer. The trick is to do these initial refactorings very conservatively.
Being conservative is the right thing to do when we can possibly introduce errors, but sometimes when we break dependencies to cover code, it doesn’t turn out as nicely as what we did in the previous example.
We might introduce parameters to methods that aren’t strictly needed in production code, or we might break apart classes in odd ways just to be able to get tests in place.
When we do that, we might end up making the code look a little poorer in that area.
If we were being less conservative, we’d just fix it immediately. We can do that, but it depends upon how much risk is involved. When errors are a big deal, and they usually are, it pays to be conservative.
When you break dependencies in legacy code, you often have to suspend your sense of aesthetics a bit. Some dependencies break cleanly; others end up looking less than ideal from a design point of view. They are like the incision points in surgery: There might be a scar left in your code after your work, but everything beneath it can get better. If later you can cover code around the point where you broke the dependencies, you can heal that scar, too.