The title of this blog post is a tip from the legendary book The Pragmatic Programmer. That is one of my favorite tips. I often interpret it as “refactor constantly”. I think we should refactor our code all the time when we are making changes to our code. That is the only way to keep code quality high.
Managers and customers are sometimes against refactoring. Why should we change the code that is running, they might think. They want to avoid paying for something that doesn’t bring them new features or fix any bugs. There has probably been some refactoring in the past that made software not to work properly. Likely all of us have experienced not working refactoring. At least I have made mistakes when refactoring (and once my manager wasn’t happy at all).
I claim that it is a way bigger mistake if we don’t refactor our code! Our code starts to smell if we don’t refactor it. For example, if we don’t split our methods ever, they get bigger and bigger by the time. That leads to long method smell. Same goes with parameters: if we don’t refactor, our methods will have one by one more parameter. As we know from Clean Code by Robert C. Martin, we should prefer zero parameters, and more than three parameters is a smelly code. That happens if we don’t ever refactor our code.
In this way, without refactoring codebase becomes more smelly and even after a short time it is really difficult to change. Those who have read 20th-century version of The Pragmatic Programmer know that key things for effective codebase are ETC: easy to change. Without refactoring, code isn’t ETC, and continuously it will be more expensive to make any changes.
According to Martin Fowler in his well known book Refactoring, there are following benefits of refactoring:
- Refactoring Improves the Design of Software,
- Refactoring Makes Software Easier to Understand,
- Refactoring Helps You Find Bugs and
- Refactoring Helps You Program Faster.
In theory, we know refactoring is good. But in practice we wake up too late to refactor: codebase is already so bad that only choice is bing-bang refactoring, which doesn’t sound good at all. No worries, I have a solution: refactor constantly.
Refactor Only with Unit Tests
There is one problem with refactoring: how can we know that we don’t break anything when we refactor the code? To know this, we have to test the code after refactoring. But isn’t it too difficult to test manually after refactoring? It depends, but usually, it is. We don’t bother to test code manually after refactoring: it takes time and is boring.
Unit tests come to rescue us! With them, refactoring can be a piece of cake. After each refactoring, we run unit tests. If they pass, we know that everything is still OK. If they fail, we know we made some error in our refactoring. Unit tests will be a really good backup for refactoring. I’d say refactoring without unit tests isn’t wise.
It is important to run unit tests after each refactoring. No matter how small our change is. That way we know immediately if we broke our code, and it is easier to fix it. If we postpone test run to the end of our refactoring session, and if tests don’t pass, we might not know what change (or even many changes!) broke our code. It can be difficult to fix it.
How to Refactor without Unit Tests?
There are times when there aren’t any unit tests for the code we need to refactor. How can we do refactoring without unit tests? Roy Osherove gives a good hint for this in his book The Art of Unit Testing: write integration tests first before refactoring. He suggests the following process:
- Add one or more integration tests (no mocks or stubs) to the system to prove the original system works as needed.
- Refactor or add a failing test for the feature you’re trying to add to the system.
- Refactor and change the system in small chunks, and run the integration tests as often as you can, to see if you break something.
Little by little, we can refactor our code with this process and write unit tests beside it. It is a win-win: we make our code better by refactoring and we improve unit test coverage.
Michael C. Feathers defines the code without unit tests as legacy code in his magnificent book Working Effectively with Legacy Code. I really like that definition.
As you continue adding more and more tests, you can refactor the system and add more unit tests to it, growing it into a more maintainable and testable system. This takes time (sometimes months and months), but it’s worth it.Roy Osherove in his book The Art of Unit Testing
Time pressure is often used as an excuse for not refactoring. But this excuse just doesn’t hold up: fail to refactor now, and there’ll be a far greater time investment to fix the problem down the road—when there are more dependencies to reckon with. Will there be more time available then? Nope.Andrew Hunt and David Thomas in their book The Pragmatic Programmer (20th Anniversary Edition)
By refactoring early and often, we prevent our code to become difficult to understand and change. We can refactor even before our code comes smelly, or latest when we find some bad code. If we don’t refactor with “I am too busy, I don’t have time to refactor” excuse, we make the code worse and worse by the time, and that will be expensive.
Keep writing unit tests all the time, and refactoring is easier. By refactoring constantly and keeping our classes and methods small, it is even easier to write good unit tests because there is less code to test. It makes future refactoring more smooth.
The famous boy scout rule is all about this. We should refactor code when we find it and make it better.
The Boy Scout Rule: Always check a module in cleaner than when you checked it out.Robert C. Martin
- Clean Code by Robert C. Martin
- Refactoring (1st edition) by Martin Fowler
- (2nd edition is in my to-read list)
- The Art of Unit Testing by Roy Osherove
- The Pragmatic Programmer (20th Anniversary Edition) by Andrew Hunt and David Thomas
- Working Effectively with Legacy Code by Michael C. Feathers
These all are excellent books, classics. I warmly recommend them to all developers.