BlackFlame33

BlackFlame33

若无力驾驭,自由便是负担。个人博客 https://blackflame33.cn/

Why become a "non-nestingist"

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 parenthesis 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. However, as the nesting depth increases, it significantly affects code readability and logic clarity. We should strive to keep the nesting depth below 2 as much as possible.

Two Methods to Eliminate Nesting#

  1. Extracting. This involves extracting code blocks that handle the same task in a method into separate sub-methods.

  2. Reversing. This involves reversing the if-else statement to make the method return as early as possible.

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 layers of nesting. By placing the negative condition branch at the beginning, we can make the program return as early as possible 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 try to handle the "negative" conditions first and return as early as possible. This way, we create a "guardian" for validation. It's like declaring the requirements of a method in advance, and only executing the core part of the method that implements the functionality after the requirements are met.

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 duplicate code is ringing!

Here is the refactored code (using a new feature in the JDK version, 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 code complexity will increase and readability will suffer;

  • Try to maintain a nesting depth of less than 2. If it exceeds 2, consider refactoring the code;
  • There are two methods to reduce nesting depth: extraction and reversal. Reversal 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;
  • It is important to have method names that are self-explanatory;
  • 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 particularly important in collaborative development, where ensuring functionality is the top priority, followed by good readability.

The Beauty of Code

Pattern Matching in Java

🌟After One Year of Work, I Reinterpreted "Refactoring"

How to Effectively Solve Code Complexity

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.