Basic Rules of Refactoring
Blog post series
This blog post is part of a series about legacy coderetreat and legacy code techniques you can apply during your work. Please click to see more sessions about legacy code.
This technique is useful to minimize the mistakes one does while changing existing code. When refactoring existing code, one can introduce defects because of rushing through the changes.
During the next sections you will find some rules (guidelines) and some explanations about why these rules are useful.
The following steps are very useful when moving code from one place to another. These steps are extremely useful if your code editor does not have automated refactoring tools. But even if your code editor has these refactoring tools, try to use these rules to check if the automated refactorings are safe enough.
The basic rules of refactoring are very useful during any refactoring activity:
- when we have tests covering the code
- for changing just the structure of the code
- without changing the functionality
- Write some system tests like in here
- Duplicate implementation
- Cover duplicated implementation with tests
These are characterization tests
Refactor the code if needed
Make sure you have enough coverage
- Reroute original code to new code
- Remove old implementation
- Check all tests pass
- Test manually
Let’s take the steps one by one:
Step 1: System tests
In order to make sure the following changes will not introduce defects, we need to focus on writing some system tests first. If the system’s design permits we can also write some unit tests, integration tests, component tests or other types of tests if needed.
It is important to have these tests to cover only the part of the system we will change, and not more.
We need to check if the tests we wrote cover enough of the code base.
Step 2: Duplicate Implementation
Whenever some part of the code needs to be refactored, we need to extract that part of the code. First of all we need to duplicate the code. We need to chose a part of the code that is independent, or that can be easily extracted to a different place.
Make sure the extracted code is small enough so you will be able to integrate fast with the big code base. The larger the duplicated code base, the more likely to have problems with integration at the end.
Step 3: Cover duplicated implementation with tests
Once the code has been copied to a different place, we need to cover the extracted code with tests. If we can we should start with unit tests. If there are static dependencies we need first of all to write some component tests. After writing component tests we can extract the static dependencies and write unit tests. At the end we might want to write some acceptance tests, if the duplicated code needs that.
All these tests are characterization tests. The tests written on existing code help us characterize the system. We focus on understanding the system and documenting it with automated tests.
After we have all these tests we can refactor the code to have less coupling, better abstractions and a clear intention of the code. We need to keep the same input and output interfaces (contracts) as this code will be called in the next step by the bigger system.
We need to constantly check if we have enough tests for the code we have just extracted. For that we can use a static test coverage analysis, or if the code is small enough we could just make some complexity computations.
Step 4: Reroute code
Once we wrote enough tests for the code we duplicated we can call the duplicated and tested code from the initial big code base. We need to run the initial system tests to make sure the code now does the thing it was supposed to.
If the tests are not green, we need to see what the problem is. The safest thing is to reset the changes in order to see where the mistakes came from. Even though reseting seems tough, it is the safest step to take. If you want to make sure that you will not introduce mistakes, just reset and learn from this.
If you will not reset, you need to understand perfectly the risks you are taking.
In the ideal case, nothing bad will happen and all the tests will be green. This means that we took small and safe steps, or we were just lucky.
Step 5: Remove old implementation
The tests pass, the code was extracted, so we do not need the initial code that was duplicated. This is the moment to remove the old code.
Some code editors will show us a warning that we have unused code. All of it must go away.
Step 6: Test the whole system
We have deleted all the unused code, and now it is the time to run all the test suites: unit tests first, component tests, integration tests, system tests, acceptance tests. If everything is green, it means that we have most likely made a safe refactoring.
Step 7: Test manually
Even though we have a lot of automated tests, we still need to test manually. Follow a test plan with a couple of manual testing scenarios both for happy path and exceptions. If everything is fine, we can say we have done a great refactoring and the code is better.
If we have some minor issues we can fix them on the spot. But remember to make sure they are small and easily fixable.
If we have major issues, the reasonable thing is to reset and start all over. Then we can analyse what were the mistakes and learn from them. In order to make sure this step is not waste, but learning we need to retrospect on what were the good and bad decisions on the way.
These steps are for ensuring a high level of safety while refactoring.
By following these guidelines we can minimize the waste of fixing bugs, but with the cost of a time investment in small and safe steps.
Remember to commit after each step. Remember to commit during each step. Committing often with good messages helps you minimize the risks and lets you focus on one simple thing at a time.
These steps are guidelines, so feel free to change them as you feel fit. But try them as they are again and again until you find a way to improve them. They are difficult to respect in the beginning, but they ensure safety and a constant pace.
If you have a code editor that can do these things automatically, make sure you have tests covering the implementation. Do not believe that the code editor is bug-free. Or sometimes it can happen that the automated refactorings are thought for a different purpose than how you are using them.
The basic rules of refactoring are very useful for legacy code. But they are useful as well when refactoring a code base covered already by tests. By doing small and safe steps we can have a steady pace, without needing to understand our last changes or fix strange bugs.
Sometimes you may feel that you do not need to take small and safe steps. Be aware that we often minimize the complexity of things. It is better to be safe than sorry.
I found about the basic rules of refactoring from JB Rainsberger during the first Legacy Coderetreat I attended in Belgium. I have used them since then, and I am very happy they decreased dramatically the number of my programmer’s mistakes (or bugs) when working with legacy code.
Please find here a code cast in Java about this session
Image credit: http://upload.wikimedia.org/wikipedia/commons/0/02/Workplace_Safety_Signs.jpg