We’ll look at two refactorings suitable for conditional statements: Pull Up Common Head of Conditional and Pull Down Common Tail of Conditional.
I originally learned these as performance optimizations but they also help simplify and clarify code.
In Refactoring, Martin Fowler describes these as “Consolidate Duplicate Conditional Fragments” but goes into less detail.
Pull Up Common Head of Conditional
You have an if
, if-elseif
, or switch/case
statement where the code at the start of each branch is identical.
So: Pull up the common code.
if (cond-expr) head head ? if (cond-expr) if-clause <=> if-clause else else head else-clause else-clause endif endif
Note: The descriptions will say “if
” but the refactoring applies to if-elseif
and switch/case
statements as well.
Limitations
This refactoring is legal if cond-expr and head have no influence on each other. (See the description of Swap Statement since it’s related.)
Here’s an example where the refactoring isn’t valid.
The Starting Code | A Non-Refactoring |
---|---|
|
|
The modified code moves identical code above the if
but it is not equivalent since it sets x
to 7 before checking it.
Mechanics
Check before refactoring:
- Check that the head code is identical in all branches.
- Check that the head code is self-contained; it can’t contain unterminated statements (such as the first half of a
while
statement). - Check that the conditional expression and the code to be moved don’t change any overlapping objects. If so, this refactoring does not apply.
The fast way:
- Copy the head code from one branch to before the
if
. - Delete the head code from both branches.
- Compile and test
-or- the safer way:
- Extract the conditional expression into a local variable
condition
. - Compile and test.
- Move the common head code to between the assignment of
condition
and theif
. - Delete the common head code from the branches.
- Compile and test.
- Swap Statement [if legal!] so that the local variable is below the head code and next to the
if
again. - Compile and test.
- Inline the
condition
variable. - Compile and test.
Example
Here’s the “safer” way step by step.
Initial code:
if (year != 0)
StringBuffer text = new StringBuffer()
text.append(title)
text.append(" (")
text.append(year)
text.append(").")
formattedTitle = text.toString()
else
StringBuffer text = new StringBuffer()
text.append(title)
text.append(".")
formattedTitle = text.toString()
endif
=> Introduce local variable (condition
)
bool condition = year != 0
if (condition)
StringBuffer text = new StringBuffer()
text.append(title)
text.append(" (")
text.append(year)
text.append(").")
formattedTitle = text.toString()
else
StringBuffer text = new StringBuffer()
text.append(title)
text.append(".")
formattedTitle = text.toString()
endif
=> Pull up the common head and Delete the common head from both branches
bool condition = year != 0
StringBuffer text = new StringBuffer()
text.append(title)
if (condition)
text.append(" (")
text.append(year)
text.append(").")
formattedTitle = text.toString()
else
text.append(".")
formattedTitle = text.toString()
endif
=>? Swap Statement – careful – not always legal! It’s easy to see it’s safe in this case, but it’s trickier when the conditions are complicated and the common code is longer.
StringBuffer text = new StringBuffer()
text.append(title)
bool condition = year != 0
if (condition)
text.append(" (")
text.append(year)
text.append(").")
formattedTitle = text.toString()
else
text.append(".")
formattedTitle = text.toString()
endif
=> Inline Variable
StringBuffer text = new StringBuffer()
text.append(title)
if (year != 0)
text.append(" (")
text.append(year)
text.append(").")
formattedTitle = text.toString()
else
text.append(".")
formattedTitle = text.toString()
endif
Example – Going the Other Way
Sometimes we want to run the refactoring backwards, pushing code from before an if
to inside it.
Initial Code:
if (type == STAR) stars++ endif if (type == STAR) text.append("*") text.append(name) else text.append(name) text.append(" (supporting)") endif
Check Conditions: The first if
statement is self-contained, and changes nothing that the second condition (type == STAR
) looks at, so we can proceed.
=> Pull in the code (to be the new common head)
if (type == STAR) if (type == STAR) stars++ endif text.append("*") text.append(name) else if (type == STAR) stars++ endif text.append(name) text.append(" (supporting)") endif
The refactoring is officially done, but we can do more:
=> Delete the Redundant Test: The first inner condition is always true, the second one never is:
if (type == STAR) stars++ text.append("*") text.append(name) else text.append(name) text.append(" (supporting)") endif
If you’ve been coding for any length of time, you probably realized you could just move stars++
into the proper branch without all this fuss. But when the statements are bigger and the conditions are more complex, it’s safer to go in smaller steps.
Pull Down Common Tail of Conditional
You have an if
, if-elseif
, or switch/case
where the code at the end of each branch is identical.
Pull down the common tail code.
if (cond-expr) if (cond-expr)
if-clause if-clause
tail else
else <=> else-clause
else-clause end
tail tail
end
Be extra careful if the tail code uses variables declared in the scope of the if
or else
branches.
Mechanics
Check before using:
- Check that the tail code is identical.
- Check that the tail code consists of whole statements. (You can’t extract the last half of an inner
while
loop, for example.) - Significant local variables are those that are declared in the if-clause or else-clause preceding the common tail, and are referenced in the tail. If there are any, check that they have identical types.
- Useful technique if you have automated refactorings: Start (but don’t finish) an Extract Method on the tail code; the tool will show you if there are incomplete statements or issues with mismatched types.
The steps:
- If there are any significant local variables, pull up their declaration (but not their initialization) to before the
if
. - Compile and test.
- Copy the tail code to after the
if
. - Delete the tail code from each branch of the
if
. - Compile and test.
Example
Initial Code:
if (type == STAR) StringBuffer text = new StringBuffer("*") text.append(name) text.append(".") formattedName = text.toString() else StringBuffer text = new StringBuffer(name) text.append(" (supporting)") text.append(".") formattedName = text.toString() endif
Check that the tail code is identical: The last two lines of each branch are the same.
Check that the tail code is whole statements: No issues there.
Check that significant local declarations are the same: The text
variable has the same type in both branches. (Had one been a StringBuffer
and the other a StringBuilder
, we couldn’t do the refactoring yet. Or if text
were declared both before the if
and inside only one branch, we couldn’t do the refactoring yet.)
=> Pull up declaration of significant local variables
StringBuffer text; if (type == STAR) text = new StringBuffer("*") text.append(name) text.append(".") formattedName = text.toString() else text = new StringBuffer(name) text.append(" (supporting)") text.append(".") formattedName = text.toString() endif
=> Pull down the common tail and Delete the tail from both branches
StringBuffer text; if (type == STAR) text = new StringBuffer("*") text.append(name) else text = new StringBuffer(name) text.append(" (supporting)") endif text.append(".") formattedName = text.toString()
Summary
Conditionals increase the complexity of code, and often hide duplication. Refactorings to Pull Common Code from Conditional remove duplication and usually make the code easier to understand.
—–
Thanks to Nayan Hajratwala, Justin Knowles, and Rick McAdams for comments on an earlier draft.