To refactor is to systematically “improve the design of existing code”. There are many ways to classify refactorings, but one distinction stands out to me: local refactorings. A local refactoring affects only one file (and perhaps its tests).
Why Local?
Local refactorings have a few advantages. They are:
- small
- quick to implement
- easy to verify (manually if you must)
- almost always clear improvements
Local refactorings tend to be small – mostly affecting only a few lines, all in one file. They’re usually quick to implement. Ideally, your IDE will automate their implementation. (Though that’s unfortunately not universal.)
Even if you have to do these refactorings manually, you’re only comparing one file to its predecessor. This makes it easy to spot typos and “think-os”. Even better, if you’re using your IDE’s built-in refactorings, the odds of an error are even lower. (Automated refactorings can have bugs and limitations, but local changes hit those less often.)
Many larger refactorings are reversible because they represent tradeoffs – you have to assess whether the old way or the new is better. You may change a number of files, and realize (today or in the future) that you’ve gone down a worse path.
In contrast, local refactorings give you immediate feedback. For example, if an extracted method is less clear, you hit undo and move on.
Local refactorings are almost always clear improvements; a few minutes of work make your code noticeably better. This makes it easy to apply the campground rule: always leave the code a little better than you found it.
Limits of Local Refactorings
Local refactorings don’t change the object-oriented structure of your code. If you need to split or merge objects, these refactorings aren’t for that.
I think of local refactorings as getting your code to “decent structured programming”: if your class has long methods and little abstraction, the local refactoring techniques can help dramatically.
The goal of local refactorings is not to be all you do. They provide local help, and they can tee up more global improvements. But it’s very common that more global work is needed.
Rearranging Code
Reformat: Format the code according to house style (or defaults). This makes a great first step, worth checking in so future diffs look less dramatically different.
Remove Useless Comments: Get rid of comments that are misleading, wrong, or contain dead code.
Reorder Fields and Methods: Your code should tell a story as you read it.
I see three approaches:
- Random, or unsystematic
- High-level to low-level
- Low-level to high-level
Random or unsystematic is the least helpful of all. (Surprise!:)
With OO code, my default approach is high-level to low-level: first fields, then public methods (“entry points”), then methods supporting them, and helper methods at the bottom. As you read, you go from “what am I doing?” to “how am I doing it?”.
Bottom-to-top works sometimes as well. It’s kind of like when you have a complicated proof, and proving a few lemmas before you get going. Then it’s “understand these things, and the rest will make sense.”
Conditionals
Martin Fowler describes a number of refactorings for conditionals, and see “Pull Common Code from Conditional” in the References. Here are some others:
Invert Condition: This flips the “if” and “else” clauses of a conditional.
if (condition) { if (!(condition)) { clause1 clause2 } else { <=> } else { clause2 clause1 } }
Nest or Un-Nest Conditional: Use the meaning of && or ||. The new arrangement may allow conditions or clauses to be simplified.
if (A && B) { if (A) { clause1 <=> if (B) { clause1 } } ... }
if (A || B) { if (A) { clause1 <=> clause1 } else if (B) { clause1 // again } ... } ...
The latter transformation is amenable to “Duplicate and Customize” (see References below).
Add Guard Clause: Use early returns to reduce nesting of code.
if (A) { if (!A) { return } do-a-bunch-o-stuff <=> do-a-bunch-o-stuff } else { return }
Simplify Booleans: Get rid of double negatives and/or apply DeMorgan’s Law. (You may have to add parentheses depending on the operators used.)
!!A <=> A !(A || B) <=> !A && !B !(A && B) <=> !A || !B
Fields
Extract Constant / Field / Variable: Extract an expression to local or class scope, as a constant, field, or variable.
Encapsulate Field: Provide private get() and set() methods, then replace all other access with the methods. (This is a local refactoring for private fields.)
Rename Field: If field is private.
Methods
Extract Method: Improve communication and reduce duplication. Strive for a “Composed Method”, where everything is at the same level of abstraction.
Inline Method: The opposite of Extract Method; only local for private methods.
Local Rename Method: For a local refactoring, you need to leave the public interface untouched. You can rename by extracting the whole method, and giving that the new name. To later rename publicly (a non-local refactoring), you can inline the original method.
Harmonize Methods: You sometimes can make two methods identical, then replace one version. If the methods are both in the same class, this can be a local refactoring. (See references).
Local to Non-Local
These are many other local refactorings: change the way temporary variables are used, change parameters, improve loops, etc. As with rename, some refactorings come in local and non-local versions, where the local version may extract a local copy and improve it.
Large classes often have other objects hidden inside. So, the next level out is things like extracting helper classes. They’re not as strictly local as some of those refactorings above, but still have limited scope.
Next, you may seek out refactorings that work within an inheritance hierarchy. (
Beyond that, you have access to the full range of refactorings.
How broadly to refactor is a decision for you as you develop and improve the code. There’s nothing honorable about making only local changes; you need to refactor to what’s required in your judgment.
Conclusion
Local refactorings are mostly a subset of regular ones. The only exception is that sometimes a public interface is preserved, and the refactoring happens “beneath”.
Local refactorings provide a different focus: how much can you reduce duplication and improve clarity just working in one file at a time?
The result doesn’t get you to the best possible object-oriented code (as that requires working across multiple classes), but it provides an easy way to start, with clear and immediate payback.
I use local refactorings as a starting point: once the obvious local problems are cleaned up, I can more easily focus on global issues,.
References
“Duplicate and Customize“, by Bill Wake. https://xp123.com/articles/refactor-duplicate-and-customize/. Retrieved 2021-02-02.
“Harmony and Identity: Generalizing Code“, by Bill Wake. https://www.industriallogic.com/blog/harmony-and-identity-generalizing-code/. Retrieved 2021-02-01.
Refactoring, 2/e, by Martin Fowler.
“Refactoring: Pull Common Code from Conditional“, by Bill Wake. https://xp123.com/articles/refactoring-pull-common-code-conditional/. Retrieved 2020-02-02.