Local Refactorings

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).

A tangle of ropes

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.

A fancy knot

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:

  1. Random, or unsystematic
  2. High-level to low-level
  3. 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.