Refactoring techniques take you from one point in the solution space to another: the behavior of the code does not change (it does exactly what it did five minutes earlier) but the number and properties of objects or messages or their relationships are changed. Many of the basic steps to move in the solution space are codified as they are paradigm-specific and not domain-specific: they work in every object-oriented applications, no matter if it's about payments or transportation or restaurants.
A misconception of refactoring techniques is their directionality: it's your responsibility to choose a direction to move towards. Concretely speaking, Fowler codifies Extract Method and Inline Method for you and gives you a checklist for performing them on your code. But it's your job to understand when to extract more code and keep a single copy of a method or to inline it in several places to let the methods evolve differently.
In theory, you can refactor to improve any non functional property of the code, such as performance or robustness, but the literature and most of the Agile movement are mostly interested in the improvement in maintainability.
Mainly, refactoring actions oriented to maintainability can intervene on two axis:
- duplication. Constantly removing duplication makes you able to still change the code in the future without having to hunt down dozens of places where the same line has been pasted.
- clarity. From renaming to redistributing responsibilities, capturing more concepts in the code improves reading and comprehension time while trading off a bit of creation time (since every class or function will be read much more times than be rewritten.)
The famous rules of simple design contains these two tenets as the central part, assuming that under the assumptions of good engineering practices like testing, local optimizations of duplication and clarity will incrementally lead towards a design aligned with the forces of the problem. In this case, thanks to reuse the cost of changes becomes lower with time instead of increase.
Uncle Bob says that you don't explain to management why you need to write while() or for() cycles: you just do it as part of the job. In the same way, you don't explain to management why you need to constantly refactor the code after each modification: it's part of the process for bringing a user story to completion.
In many scenarios, refactoring is aimed at removing technical debt: a form of debt incurred to quickly get out features while overlooking conceptual integrity and continuous refactoring. Just like real debt, technical debt may appear in a good or bad form depending on the actual results (successful startups are often famous for both their financial and technical debt.)
However, it's simple to remove technical debt early in its life, just after having written a dozen new lines of code. It becomes more difficult the more the design solidifies with time: you can't easily add a method to an interface implemented by hundreds of classes; nor you can change the same decision of using an array as the main data structure after all your codebase relies on it due to a lack of encapsulation.
The TDD cycle tells you to constantly refactor as much as you can, incorporating new knowledge you have acquired while introducing the last features. Too often I see the cycle implemented as a series of red-green (introducing and test and making it pass) while skipping the final refactoring phase. I suggest you to also try the refactor-red-green cycle where you refactor before introducing a new feature to bring the design closer to supporting it. It's easier to avoid technical debt this way, but you need a stronger spidey sense for design as you need to know where to go even without the next test feedback, which is kept as a skipped one to maintain a green bar.
Next time, we will introduce automated testing as a practice that complements and enables relentless refactoring.
Meanwhile, try to pick up the habit of stopping every few minutes to think about a non-functional property of the code like its maintainability. It's important to make it work, as the mantra should be:
Clean code that works.
You shouldn't try to improve both aspects at the same time, but both of them are important for your code to live a long and happy life.