BlackFlame33

BlackFlame33

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

なぜ「非ネスト主義者」にならなければならないのか

「ネストしない主義者」とは何か#

ネストしない主義者は絶対にコードをネストしません!
まあ、できるだけネストしないように

ネストの深さ#

もし私たちが各左括弧を新しいネストのレベルの印とするなら、以下のコードブロックはネストの深さが 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;
    }
}

このメソッドはまだ論理的にシンプルです。ネストの深さが大きくなると、コードの読みやすさや論理の整理に大きな影響を与えます。私たちはできるだけネストの深さを 2 以下に抑える必要があります。

ネストを排除する 2 つの方法#

  1. 抽出。つまり、メソッド内で同じことを処理するコードブロックをサブメソッドに抽出します。

  2. 反転。つまり、if-else文を反転させて、メソッドが早くreturnするようにします。

1. 抽出#

抽出を使って、上記のコードの構造を最適化してみましょう:

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;
    }
}

見た目はあまり良くなっていないようですが。。。少なくとも今はforループの内部でフィルタリングされた数字が累積されていることが一目でわかります。

2. 反転#

次に、反転を使います。「正の」条件の分岐を深いところに置くと、コードは多くのレベルでネストされます。「負の」条件の分岐を前に置くことで、プログラムは早く戻ることができ、else部分のコードをネストする必要がなくなります。

試してみましょう:

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;
    }
    // 反転後、プログラムがここまで実行できるということは、top > bottomであるため、インデントを1つ減らすことができます
    int sum = 0;
    for (int number = bottom; number <= top; number++)
    {
        sum += filterNumber(number);
    }
    return sum;
}

複数の条件検証がある場合、条件を反転させて「負の」条件を早めに処理し、早く戻るようにします。こうすることで、「検証ガード」を形成します。まるで、メソッドの要求を事前に宣言し、要求を満たした後にメソッドの機能のコア部分を実行するかのようです。

このように、「正の」条件のコードは下にあり、「負の」条件のコードはすべてインデントされています。

メソッドのコア部分を読むとき、現在のメソッドの状態を覚えておく必要もありません。


練習#

次の「傑作」を見てみましょう:

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;
}

直接頭皮がむずむずします、しかし実際にはこのコードは見た目は複雑ですが、論理は明確です。ただし。。。すべての処理ロジックが 1 つのメソッドに詰め込まれているため、見た目が「驚くべき」ものになっています。

元のコードでvalueの繰り返しの判断やnewWxpressionの繰り返しの代入、さらにDoubleFloatの処理ロジックが一致していることに気づきました。ああ、重複コードの警笛が鳴りました!

以下はリファクタリング後のコードです(ここでは jdk の新しいバージョンの機能を使用し、switch が値を返すことができます):

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;
}
// getNewExpression() の具体的な実装

リファクタリング後のコードはより簡潔で、可読性も高くなりました。もちろん、例のコードはif-elseよりもswitchを使用する方が適しています。if-elseタイプに対しては、反転を使用することもできます。

最後に#

詳細が多すぎると、コードの複雑さが増し、可読性が低下することを理解できます;

  • ネストの深さはできるだけ 2 以下に保ち、2 を超える場合はコードのリファクタリングを検討する必要があります;
  • ネストの深さを減らすための 2 つの方法があります:抽出と反転。反転は「負の」条件のコードを早く戻らせ、メソッドのコア部分を下に保ちます;抽出はコードの論理を整理し、詳細を知らなくても済むようにします;
  • 意味が明確なメソッド名は非常に重要です;
  • メソッドはできるだけ 1 つのことを行うべきであり、メソッドが長すぎる場合(50 行は比較的良い判断基準です)、可読性のためにコードをサブメソッドに抽出する必要があります;
  • メソッドの再利用が多く、パラメータが少ないほど、抽出がうまくいっていることを示しています。
  • これは協力開発において特に重要であり、機能の実装を保証することが最優先であり、次に良い可読性が求められます。
関連リンク:

コードの美

Java のパターンマッチング

🌟1 年働いて、私は『リファクタリング』を再理解しました

コードのサイクル複雑度を効果的に解決する方法

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。