What is defactoring?
Defactoring is actually refactoring, but it looks and feels wrong because it seems like reverse refactoring. Defactoring is refactoring code toward a seemingly worse state, such as one with duplicate code and code that has moved from higher to lower level abstractions. I once shied away from defactoring, feeling like I must be committing some crime, but now I embrace it, under certain circumstances.
Why would I want to defactor?
One reason to defactor code is to get it into an intermediate state where it will be easier to refactor into something better. Sometimes I can apply refactorings incorrectly before I realize it. For example, I can accidentally extract a method or even a class when I don’t have a clearly defined abstraction in mind. Maybe I think this and that chunk of code should be put together, but maybe that’s wrong. Maybe those two things do not really go with each other. Maybe just one of them belongs over there, while the other one should stay in its original place.
When does defactoring happen?
There’s two time periods when defactoring needs to happen. The first is during initial coding. The second is much later, after a chunk of code has been around awhile and it is proving to be a royal pain in the ass (RPITA).
During initial coding
I believe that committing often is A Good Idea (TM). So I will commit frequently each day. I can always squash commits when the day is done. One reason this is A Good Idea (TM) is I don’t trust myself. Until I see good results on the screen in front of me, I’m not going to assume that I’m making the right decisions. By committing often, I give myself little checkpoints that I can go back to in case I go down a wrong path.
Under this model, I can constantly evaluate my code with one of two methods:
- Look back at the commits, identify how I went wrong, and fix forward by defactoring and then making a new commit.
- Simply revert back to a given commit and start over.
Neither way is necessarily right. Both serve well under different contexts. The point is to have a way to quickly exit out of a wrong direction and get going in a right direction.
When code is a RPITA
Usually code is a RPITA because it is getting touched a lot. Code that gets touched a lot is smelly. It is probably tightly coupled with multiple other chunks of code. Changes elsewhere are exerting too much backpressure here. The code needs better separation of concerns!
How can I achieve that? Often the answer is defactoring. I need to take the code that is here in this class, which is changing all the time because other classes that depend on it are changing all the time, and I need to break the dependency. A good first step might be to defactor. Duplicate the logic out into those dependent classes. This will break the dependency (good!) but result in duplication and degeneralization (bad). (By “degeneralization” I mean type parameters will turn into concrete types, inheritance trees will turn back into separate single classes, and so on and so forth.)
The code thus ends up in a seemingly devolved state at the end of this process. Now it might be easier to see how to refactor it in a new and better direction. The previous state of the code either abstracted too many things together in a way that didn’t make sense, or it was just trying too hard to generalize many specific and different things as though they were one same thing.
Whatever the case may be, defactoring helps me see why the code was a royal pain in the ass and then fix it.
Don’t be afraid to throw away effort
Sometimes the result of defactoring is worse than the refactoring. OK. When that happens, I throw the effort away. It’s easy to go back in time to the “refactored” state if I commit often. I consider lost effort a valuable learning experience. Just because the resulting code gets lost forever, doesn’t mean that I am not a better person at the end of it. I probably understand more about the business, more about the code, and more about the practice of programming after throwing code away.
Defactoring for readability and maintainability
Defactoring can also have the opposite effect. Just because you move code away from high level abstractions like higher-order functions and class composition towards simpler abstractions like methods, doesn’t mean you will always be going towards merely an “intermediate” state. Sometimes you actually land somewhere better. I’ve recently seen inner classes inheriting from other inner classes, and overriding methods just to call other methods in the enclosing class, all just to provide a way for four methods to share code by creating anonymous classes. It was overcomplicated.
“Defactoring” this code made it a lot more readable. And, yes, that means that those four methods now had code that looked very similar. So be it. They are all next to each other in the same class and they are doing similar things. I don’t think it’s worth refactoring them further. The defactored code had fewer lines overall even if there is some repetition in the general idea.
So that’s why I defactor as much as I refactor.
You probably call it all refactoring. Whatever. I like the term I’ve coined for it and whatever it’s called, I’m going to keep doing it until / unless I find something better.
It works really well for me. Your mileage may vary. Happy coding!