What is a "Non-Nester"#
Non-Nesters never nest their code!
Well, try not to as much as possible
Nesting Depth#
If we consider each left bracket as a new level of nesting, the following code block is a method with a nesting depth of 4:
public int calculate(int bottom, int top)
{ // 1 😎
if (top > bottom)
{ // 2 🤨
int sum = 0;
for (int number = bottom; number <= top; number++)
{ // 3 🤔
if (number % 2 == 0)
{ // 4 😡
sum += number;
}
}
return sum;
}
else
{
return 0;
}
}
This method is relatively simple in terms of logic. As the nesting depth increases, it significantly affects code readability and logic clarity. We should try to keep the nesting depth below 2 as much as possible.
Two Methods to Eliminate Nesting#
-
Extracting. This involves extracting code blocks that handle the same task in a method into sub-methods.
-
Reversing. This involves reversing the
if-else
statement to make the method return early.
1. Extracting#
Let's use extraction to optimize the structure of the above code:
int filterNumber(int number)
{
if (number % 2 == 0)
{
return number;
}
return 0;
}
public int calculate(int bottom, int top)
{ // 1
if (top > bottom)
{ // 2
int sum = 0;
for (int number = bottom; number <= top; number++)
{ // 3
sum += filterNumber(number);
}
return sum;
}
else
{
return 0;
}
}
It doesn't look much better... but at least now I can see at a glance that the numbers filtered within the for
loop are being accumulated.
2. Reversing#
Next, let's try reversing. When you place the positive condition branch deeper in the code, it results in many levels of nesting. By putting the negative condition branch first, we can make the program return early without nesting the code in the else
part.
Let's give it a try:
int filterNumber(int number)
{
if (number % 2 == 0)
{
return number;
}
return 0;
}
public int calculate(int bottom, int top)
{ // 1
if (top <= bottom)
{ // 2
return 0;
}
// After reversing, since the program can reach this point, it means top > bottom, so we can reduce one level of indentation
int sum = 0;
for (int number = bottom; number <= top; number++)
{
sum += filterNumber(number);
}
return sum;
}
By continuously reversing conditions when we have multiple condition checks, we can handle the "negative" conditions first and return early. This way, we create a "guard clause" that declares the requirements of the method in advance. If the requirements are met, the core part of the method is executed.
In this way, the code for the "positive" conditions is placed below, while the code for the "negative" conditions is indented.
When reading the core part of the method, we no longer need to remember the current state of the method.
Practice#
Let's take a look at this "masterpiece":
private static String getValueText(Object value) {
final String newExpression;
if (value instanceof String) {
final String string = (String)value;
newExpression = '"' + StringUtil.escapeStringCharacters(string) + '"';
}
else if (value instanceof Character) {
newExpression = '\'' + StringUtil.escapeStringCharacters(value.toString()) + '\'';
}
else if (value instanceof Long) {
newExpression = value.toString() + 'L';
}
else if (value instanceof Double) {
final double v = (Double)value;
if (Double.isNaN(v)) {
newExpression = "java.lang.Double.NaN";
}
else if (Double.isInfinite(v)) {
if (v > 0.0) {
newExpression = "java.lang.Double.POSITIVE_INFINITY";
}
else {
newExpression = "java.lang.Double.NEGATIVE_INFINITY";
}
}
else {
newExpression = Double.toString(v);
}
}
else if (value instanceof Float) {
final float v = (Float) value;
if (Float.isNaN(v)) {
newExpression = "java.lang.Float.NaN";
}
else if (Float.isInfinite(v)) {
if (v > 0.0F) {
newExpression = "java.lang.Float.POSITIVE_INFINITY";
}
else {
newExpression = "java.lang.Float.NEGATIVE_INFINITY";
}
}
else {
newExpression = Float.toString(v) + 'f';
}
}
else if (value == null) {
newExpression = "null";
}
else {
newExpression = String.valueOf(value);
}
return newExpression;
}
My head is spinning, but actually, although this code looks complex, the logic is still clear. It's just... all the processing logic is in one method, which makes it look intimidating.
Observing the repeated checks for value
and the repeated assignment of newExpression
, and noticing that the handling logic for Double
and Float
is the same, oh, the alarm for duplicated code is ringing!
Here is the refactored code (using a new feature in the JDK version, where switch
can return a value):
private static String getValueText(Object value) {
final String newExpression = switch (value) {
case String string -> '"' + StringUtil.escapeStringCharacters(string) + '"';
case Character character -> '\'' + StringUtil.escapeStringCharacters(value.toString()) + '\'';
case Long aLong -> value.toString() + 'L';
case Double aDouble -> getNewExpression(aDouble);
case Float aFloat -> getNewExpression(aFloat);
case null -> "null";
default -> String.valueOf(value);
};
return newExpression;
}
// Implementation of getNewExpression()
The refactored code is more concise and readable. Of course, the example code is more suitable for using switch
to handle. If it were for if-else
types, we could also use reversal.
Finally#
It can be understood that if too many details are exposed, the complexity of the code will increase and the readability will be poor;
- Try to maintain the nesting depth below 2. If it exceeds 2, consider refactoring the code;
- There are two methods to reduce nesting depth: extraction and reversal. Reversing allows the code for "negative" conditions to return early, so that the core part of the method is placed below; extraction helps us clarify the logic of the code without having to know the details;
- Meaningful method names are important;
- A method should ideally only do one thing. If a method is too long (50 lines is a good criterion), it is necessary to extract the code into sub-methods for readability;
- The more a method is reused and the fewer parameters it has, the better the extraction.
- This is especially important in collaborative development. In addition to ensuring the implementation of functionality, good readability is also important.
Related Links:
🌟After One Year of Work, I Have a New Understanding of "Refactoring"