This article describes a technique that I believe is missing from our practice, especially from teams working on complex (legacy) systems.
I can't refactor this code because I don't know what happens in there.
Perhaps you were trying to implement an apparently minor change, perhaps hunting a bug brought you to that code, or maybe you had some performance tweaking to do, either way, you ran one day into some code you knew nothing about and suddenly realized that you had to change it. Not superficial polishing like extracting a method or renaming a variable. No! A much deeper and scary refactoring of unknown code.
Fear is normal, it's actually beneficial. But the analysis paralysis in which you can deadlock is not.
A big part of my younger days I studied medicine with my (now) wife. It was a fascinating experience that change my perception of many things. There were some medical topics out there (like the hormone dynamics, coagulation, or the immune system mechanics) that no one could literally understand from the first read. It took us sometimes 3 or 4 passes through various topics to finally see the big picture, and really get it. I remember forcing ourselves to push through some chapters even if we didn't feel we mastered them. It was painful and frustrating, but it was the only way we could make progress. Because it was only much later that all the pieces started falling together in their places.
Today, in all the dedicated workshops that I do on Clean Code and Refactoring I ask the participants to provide me samples of production code and we mob refactor them together. It happens quite often that after 20 minutes of aggressive refactoring that declutters the code, we finally understand the big idea, or we discover a large underlying pattern. To everyone's surprise, I often revert all our work right then. BANG! Everyone is shocked.
He reverted! OMG! He lost! - I understand from their gazes
No, my dear! I won! I finally understood how the code should look and I realized that I was on the wrong path. Or perhaps I deliberately rushed for a while or I felt I did some useless or risky steps along the way.
The biggest win of Refactoring is a thorough understanding of the code.
That's what makes it addictive. The joy of revealing the true intent, getting to the essence of the problem being solved.
Indeed, refactoring often starts with tiny safe moves, that's the default way of working, covering our changes with sufficient tests to make sure we don't break anything. And it works perfectly for plenty of cases. If the code is in a good shape and the team has high code quality standards, you can probably keep in this mode for months, maybe years if you're lucky.
But occasionally, this code polishing refactoring is not enough. Perhaps the problem doesn't fit entirely in your head, or you literally can't decide between two design alternatives. When this happens, you must be prepared to enter Exploratory Refactoring.
During such an exercise you are allowed to do bad things so please commit all your (safe) work until then. Then say "Ave Maria" 3 times, set a timer to 15-30 minutes, and jump right in😊. If you are like me - snoozing the timer forever, tell your boss to call you in 30 minutes to ask for progress. Or do it right before a meeting you can't miss.
Because the amount of code editing to perform is huge, using automated refactoring offered by your IDE is highly recommended. Move Method, Change Signature, Introduce Parameter, Inline/Extract Method, Introduce Parameter Object - these are some refactoring moves that I heavily use in such exercises.
The first goal is to remove the clutter from the code as fast as possible in order to see deeper patterns. In order to better focus your exploration, you are allowed to delete tests* that impede you or unrelated production code in other packages. On some occasions, I even deleted from the explored code all the logging or some collateral functionalities like notification or error handling, in order to better see the main flow.
*About tests, I do owe huge gratitude to some expressive tests that allowed me to understand the current behavior of the code much better and faster than reading the (outdated) specification available to me.
But again, the purpose of exploratory refactoring is NOT to commit, but to allow you to understand the code by touching it. Seeing code moving around allows a much deeper understanding of the dependencies it has, of any temporal coupling involved, and of the responsibilities in play.
The best way to understand code is by touching it.
Touching the code has another subtle psychological advantage: it makes you feel in charge of the code. You get more courage, something which is often lacking in many teams on legacy codebases. Pair Programming can also prove very helpful in such exercises, as in fact for any design, or learning activity.
Exploratory Refactoring can be an individual exercise or can be applied during official hackathons like some of my client companies are using to explore refactoring ideas of their product(s).
But then, after applying this technique for many years, someone told me that what I was doing was formalized since 2014...
The Mikado Method
It turns out that the most difficult part from above is to know when to draw the line and decide 'officially' to launch for a timeboxed Exploratory Refactoring. It can be very hard to decide to sacrifice the next 30 minutes for learning the code, and again, very painful to revert your work at the end of it. In other words:
I believe you Victor, that understanding code is everything, but I still have to get my job done for the day.
Wishful thinking might make you hope you'll get away with just safe minimal-invasive changes and polishing. But then changes you do start breaking stuff; fixing that stuff breaks more stuff and so on, in a cascading chain reaction. And the bad news is that the IDE automated refactoring will break if the code is not compiling anymore.
But here comes Mikado Method to the rescue, formalizing what to do in such cases.
To summarize it in simple words: you naively try to implement your change or refactoring goal. If you fail to do so in, say, 30 minutes - if you still have errors, then express the prerequisites for getting your task done (and that's tricky sometimes). Then revert and try to tackle the prerequisite first, repeating the process. Very important is the visualization of the prerequisites discovered and their relationships. Here is the full activity diagram for this method:
What you end up with is a graph of tasks and dependencies. At the end of the exploration, you will identify some tasks simple enough to implement safely in a reasonable amount of time. After you solve those, you dig your way back up to the original task, implementing one prerequisite after the other.
Here is a great read on the topic: https://improveandrepeat.com/2020/12/the-mikado-method-a-great-help-to-work-with-legacy-code/
I truly believe our profession doesn't play enough with design. Bugs and Features always occupy a prime place in the agenda, but the design is left for later. And I find this very sad, as (re)designing the solution to a problem is one of the most pleasant activities of a developer (besides shipping working software, of course).
Restore this passion with time-boxed Exploratory Refactoring sessions with the sole goal of understanding and experimenting with design.
Give it a try, then post your comments below to tell me how it went.