How do you know when you’ve refactored enough and when you’ve refactored too much? I asked Sandi Metz about this once; here is a paraphrase of her answer:
Refactor until you feel you are one step behind the solution you want. Avoid your comfort zone. And while you might see extractions that would probably get you to a better design, don’t apply them until you see the need two or three different times. Remember that duplication is cheaper than the wrong abstraction. The best refactors will then naturally arise from the repeated inconveniences, instead of from unbacked ideas of what could potentially be better.
I became a fan of applying refactoring techniques after seeing how it made two teams in my previous company more reliable and productive over the span of two months. I read about Design Patterns, Refactoring Techniques and Patterns, Software Design, subscribed to resources on Rubyisms like RubyTapas and Upcase. I read countless blog posts on different ways of solving problems. These teams continued to ship features without any interruption while I iteratively applied design improvements to the codebase.
Although a desirable outcome, refactoring is not only about the aesthetics of your code. It’s also about sharing the understanding of the problem we are solving and how our software helps to solve that problem. With better understanding, we can more easily read and change the code, and we can even identify dead code to remove.
While thinking twice about certain portions of our software, we might discover new edge cases and bugs we haven’t seen before and opportunities to improve performance.
When source code is self-explanatory, a team can loosen its dependence on tools and processes meant to improve cross-team communication. Project management applications, Iteration Planning Meetings (IPMs), and vocabulary dictionaries, for instance, are not as necessary when developers speak the language the business owners speak.
Sometimes we are forced to refactor before adjusting any functionality in our project. Steve Freeman proposed dropping the “technical debt” analogy for “unhedged call option” to better denote the fact that we may lose control of our software before we can iteratively “pay off our debt”. We might then need to refurbish the codebase.
Refactoring is engaging and fun, too. I would refactor all day long if it always made sense.
Rewriting a class that is rarely used to make it more open for extension might be fun but very unlikely to reap benefits both in the short and the long term. Such a refactor can (and probably should) be avoided.
As Joe Ferris said in our blog post and “Ruby Science” book, extracting classes decrease the amount of complexity in each object but increases the overall complexity of the application. Extracting too many classes will create extra indirections that developers will have to navigate.
Every class also requires a name. Introducing new names can help to explain functionality at a higher level and facilitate communication between developers. However, introducing too many names results in vocabulary overload, which makes the system difficult to learn for new developers.
My answer to “how often should we refactor” is “almost always”. I find over-refactored code to be an uncommon problem, whereas overloaded code can be found around the corner. Trying to improve the design of our software once it has rolled downhill for a few months is too hard, while it’s easy to stop refactoring once we’ve seen enough.
If you apply new abstractions only in response to pain and resistance, you’ll end up with the right number of classes and names for your application and team. Go forth and refactor, until you feel almost comfortable.