web-dev-qa-db-ja.com

長いif-elseツリーを簡潔に表す方法

要するに、私はJavaこのようなメソッドで作られたコードの一部を継承しました:

@Override
public Action decide() {

    if (equalz(in.a, "LOC")) {//10
        if(( //20
                equalz(tmp.b, "BA")
                && notEquals(in.c,"U")
                && equalz(in.d,"Y")
            )||(
                equalz(tmp.b, "HV")
                && notEquals(in.c,"U")
                && equalz(in.d,"Y")
                && varEqualsOneOf(in.e,"FLG","FLR","RRG"))
        ) {
            if(equalz(tmp.b, "BA")) {//30
                if(varEqualsOneOf(in.e,"RRG","NL")//40
                    && lessThan(in.f,in.g)) {
                    return Action.AC015;
                } else {
                    if(varEqualsOneOf(in.e,"FLG","FLR")) {//50
                        return Action.AC015;
                    } else {
                        return Action.AC000;
                    }
                }
            } else {
                if (equalz(in.e,"RRG")) {//60
                    return Action.AC014;
                } else {
                    return Action.AC010;
                }
            }
        } else {
            if(equalsOrMissing(in.c,"U")    
                && varEqualsOneOf(in.e,"FLG","FLR")) {//70
                return Action.AC011;
            } else {
                return Action.AC000;
            }
        }
    } else {
        if(varEqualsOneOf(in.a,"MNC","BAN","LCI","CTV","LEA","INS")) {//80
            if (//90
                equalz(in.h,"A")
                || equalz(in.i,"Y")
            ) {
                return Action.AC000;
            } else {
                if(notEquals(in.h,"U")) {//100
                    if(greaterThan(in.j,in.k)) {//110
                        return Action.AC000;
                    } else {
                        if(varEqualsOneOf(in.e,"FLG","FLR")) {//120
                            if(notEquals(in.l,"U")) {//130
                                return Action.AC004;
                            } else {
                                if(oneOfVarsEqual(in.m,in.n,"Y")) {//140
                                    return Action.AC012;
                                } else {
                                    return Action.AC002;
                                }
                            }
                        } else {
                            if(//150
                                (
                                    equalz(in.e,"RRG")
                                    && varEqualsOneOf(in.a,"MNC","BAN","LCI","CTV","LEA")   
                                )
                                ||
                                (
                                    varEqualsOneOf(in.e,"RRG","NL")
                                    && equalz(in.a,"INS")
                                )
                            ) {
                                if (notEquals(in.l,"U")) {//160
                                    if (notEquals(in.o,"U")) {//170
                                        return Action.AC005;
                                    } else {
                                        return Action.AC018;
                                    }
                                } else {
                                    bp(false);
                                    if (equalz(in.n,"Y")) {//180
                                        return Action.AC012;
                                    } else {
                                        return Action.AC002;
                                    }
                                }
                            } else {
                                if (equalz(in.p,in.q)) {//190
                                    if (notEquals(in.o,"U")) {//200
                                        return Action.AC006;
                                    } else {
                                        return Action.AC008;
                                    }
                                } else {
                                    return Action.AC000;
                                }
                            }
                        }
                    }
                } else {
                    bp(false);
                    if (notEquals(in.l,"U")//210
                        && varEqualsOneOf(in.e,"FLG","FLR")) {
                        return Action.AC004;
                    } else {
                        return Action.AC000;
                    }
                }
            }
        } else {
            return Action.AC000;
        }
    }
}

ここで、実行する正しいアクションを返すために一連の条件がチェックされます。この解決策はあまり洗練されていません。さらに、特定の出力(DBに記録する)につながるパスをすばやく確認する方法が必要です。

コードをリファクタリングし、条件ツリーをコンパクトに表す方法を考えています。これにより、読み取り、保守、およびログが少し簡単になる可能性があります。

私は、4つの要素で構成されるすべての条件の行を持つ2次元マトリックスについて考えました

  • 条件ID
  • 現在の条件がtrueの場合の条件ID(または戻り値)
  • 現在の条件がfalseの場合の条件ID(または戻り値)
  • 状態自体

だから、最後に私はこの行列を持っています

Object[][]matrix=
    {
            {10,20,70,  
                equalz(in.a, "LOC")},

            {20,30,80,  
                (equalz(tmp.b, "BA")
                && notEquals(in.c,"U")
                && equalz(in.d,"Y")
                )||(
                equalz(tmp.b, "HV")
                && notEquals(in.c,"U")
                && equalz(in.d,"Y")
                && varEqualsOneOf(in.e,"FLG","FLR","RRG"))},

            {30,40,70,  
                equalz(tmp.b, "BA")},

            {40,Action.AC015,50,    
                varEqualsOneOf(in.e,"RRG","NL")
                && lessThan(in.f,in.g)},

            {50,Action.AC015,Action.AC000,  
                varEqualsOneOf(in.e,"FLG","FLR")},

            {60,Action.AC014,Action.AC010,  
                equalz(in.e,"RRG")},

            {70,Action.AC011,Action.AC000,  
                equalsOrMissing(in.c,"U")   
                && varEqualsOneOf(in.e,"FLG","FLR")},

            {80,90,Action.AC000,    
                varEqualsOneOf(in.a,"MNC","BAN","LCI","CTV","LEA","INS")},

            {90,Action.AC000,100,   
                equalz(in.h,"A")|| equalz(in.i,"Y")},

            {100,110,210,   
                notEquals(in.h,"U")},

            {110,Action.AC000,120,  
                greaterThan(in.j,in.k)},

            {120,130,150,   
                varEqualsOneOf(in.e,"FLG","FLR")},

            {130,Action.AC004,140,  
                notEquals(in.l,"U")},

            {140,Action.AC012,Action.AC002, 
                oneOfVarsEqual(in.m,in.n,"Y")},

            {150,160,190,   
                    (equalz(in.e,"RRG")
                    && varEqualsOneOf(in.a,"MNC","BAN","LCI","CTV","LEA")   
                    )||(
                    varEqualsOneOf(in.e,"RRG","NL")
                    && equalz(in.a,"INS"))},

            {160,170,180,   
                    notEquals(in.l,"U")},

            {170,Action.AC005,Action.AC018, 
                    notEquals(in.o,"U")},

            {180,Action.AC012,Action.AC002, 
                    equalz(in.n,"Y")},

            {190,200,Action.AC000,  
                    equalz(in.p,in.q)},

            {200,Action.AC006,Action.AC008, 
                    notEquals(in.o,"U")},

            {210,Action.AC004,Action.AC000, 
                    notEquals(in.l,"U")
                    && varEqualsOneOf(in.e,"FLG","FLR")}
    }
;

条件間をジャンプするメソッドによって駆動され、条件のIDを持つパスを表す実行全体を記録します。

初めはこれは良いアイデアかもしれないと思っていましたが、今ではそれが書き留められているかどうかはわかりません。

この問題をどのように処理しますか?私のソリューションはまったく価値がありませんか?目標を達成する他のより良い方法はありますか?

6
Luigi Cortese

私はマトリックスの考えには行きません。コードは少ないですが、読みやすくなるとは思いません。どちらかといえば、1つのことを精神的に評価できず、メソッドに残っているコードの半分を無視できないため、読みにくいと思います。私はすべてをチェックしなければなりません。

まずはネストの削減に取り組みます。たとえば、最も外側のifブロックとそれ以外のブロックを見ると、次のようになります。

if (equalz(in.a, "LOC")) {
    ...
} else {
    if(varEqualsOneOf(in.a,"MNC","BAN","LCI","CTV","LEA","INS")) {
        ...
    } else {
        return Action.AC000;
    }
}

最も外側のelseブロックには、ネストされた別のif-elseのみがあり、一般的なコードはないため、これをレベルの外に移動して、次のようにすることができます。

if (equalz(in.a, "LOC")) {
    ...
} else if(varEqualsOneOf(in.a,"MNC","BAN","LCI","CTV","LEA","INS")) {
    ...
} else {
    return Action.AC000;
}

この同じ原則をこのブロックに適用すると、次のようになります。

if(varEqualsOneOf(in.e,"RRG","NL")//40
    && lessThan(in.f,in.g)) {
    return Action.AC015;
} else {
    if(varEqualsOneOf(in.e,"FLG","FLR")) {//50
        return Action.AC015;
    } else {
        return Action.AC000;
    }
}

我々が得る:

if(varEqualsOneOf(in.e,"RRG","NL")//40
    && lessThan(in.f,in.g)) {
    return Action.AC015;
} else if(varEqualsOneOf(in.e,"FLG","FLR")) {//50
    return Action.AC015;
} else {
    return Action.AC000;
}

ちょっと待って。 ifとelse ifブランチは同じものを返します。それらを組み合わせることができます。そうすると、次のようになります。

if((varEqualsOneOf(in.e,"RRG","NL")/*40*/ && lessThan(in.f,in.g))
    || (varEqualsOneOf(in.e,"FLG","FLR") /*50*/)) {
    return Action.AC015;
} else {
    return Action.AC000;
}

条件が少し厄介になっている場合、特にすべての貧しい人々の名前とマジックナンバーがそうです。これが、条件をカプセル化して意味のある名前を付けるヘルパー関数の作成を開始する場所です。だから私はこのようなものを得るでしょう:

if(IsCondition40(in) || IsCondition50(in)) {
    return Action.AC015;
} else {
    return Action.AC000;
}
.....
// obviously these still need better names but you get the idea.  Magic strings and numbers should be replaced, etc.
// also, forgive my poor Java, not a language I use often enough to be any good at it
private boolean IsCondition40(In in) {
    return varEqualsOneOf(in.e,"RRG","NL") && lessThan(in.f,in.g);
}
private boolean IsCondition50(In in) {
    return varEqualsOneOf(in.e,"FLG","FLR");
}

そして今、それはどこかに行き始めています。それを一歩一歩分けていってください、そしてそれはより良くなり始めます。あなたが何かを壊していないことを確認するために、毎回行くたびにテストを実行することを忘れないでください(これを始める前にテストをしましたよね?)。

一般的な戻り値と条件を探し、それらを組み合わせるか、ヘルパー関数を作成します。入れ子を減らします。最終的には、もっと読みやすいものになります。

7
Becuzz
  1. それらのすべての「マジック定数」文字列を意味のある名前の定数に変更します。 「FLG」とは一体何ですか?
  2. 次に、すべての無意味なアクション名を変更します。 「Action.AC006」は意味のない名前です。
  3. 無意味なコメントを「// 20」、「// 30」...から変更します(これらがプロジェクトに取り組んでいる誰かに何らかの形で明確でない限り)。それらはACxxx番号と一致しないようです。

その後、他の回答で言及されているリファクタリングのいくつかを試してください。ゴミをリファクタリングすると、ゴミがリファクタリングされます。 :-)

そしてOOPに関するもう1つのこと

_tmp.b_の値以外に、ifステートメントのallinオブジェクト。 (私がそれを逃したのでなければ...) Tell Do n't Ask (またはこれを参照してください SO answer )は、decide()メソッドを提案しますが、書き直され、inオブジェクトに移動されます。例えば.

public Action decideAction(String whateverTmpBReallyMeans);

これはプロジェクトで理にかなっている場合もあれば、そうでない場合もありますが、検討する価値はあります。

5
user949300

私はここであなたの質問に答えないかもしれませんが、コードを読んだときに私が持っていたいくつかのひどい反射を与えることを許可します。

このコードには多くのことがあり、一見して読みにくくなっています。これは、セグメントの動作に関する理解の欠如と、奇妙に因数分解されていることが原因です。特定の出力に至るまでの流れをたどることができるとのご要望を理解しましたが、これは単に形式が正しくないコードの影響のようです。コードが追跡しやすい場合は、returnステートメントへのパスを決定するための複雑な行列は必要ありません。

手始めにこれを見てみましょう:

if(( //20
        equalz(tmp.b, "BA")
        && notEquals(in.c,"U")
        && equalz(in.d,"Y")
    )||(
        equalz(tmp.b, "HV")
        && notEquals(in.c,"U")
        && equalz(in.d,"Y")
        && varEqualsOneOf(in.e,"FLG","FLR","RRG"))
) {

これは、実際に起こっていることをよりよく説明するために、関数呼び出し内に含めることができます。次のような簡単なものでうまくいきます。

private boolean isFoobar(In in, Tmp tmp) {
     return (equalz(tmp.b, "BA") && notEquals(in.c,"U") && equalz(in.d,"Y"))
                || (equalz(tmp.b, "HV") && notEquals(in.c,"U") && equalz(in.d,"Y") && varEqualsOneOf(in.e,"FLG","FLR","RRG"));
}

これにより、チェックの再利用性が高まり、コードが読みやすくなります。これは、視覚的な混乱を減らすと同時に、関数にわかりやすい名前を付けて、実際に確認しようとしていることを読者に説明するためです。

次のセグメントも、リファクタリングされると、上記のアプローチの恩恵を受けることができます。

if(varEqualsOneOf(in.e,"RRG","NL")//40
    && lessThan(in.f,in.g)) {
    return Action.AC015;
} else {
    if(varEqualsOneOf(in.e,"FLG","FLR")) {//50
        return Action.AC015;
    } else {
        return Action.AC000;
    }
}

論理的に同等です:

if((varEqualsOneOf(in.e,"RRG","NL") && lessThan(in.f,in.g))
        || varEqualsOneOf(in.e,"FLG","FLR")) {
    return Action.AC015;
} else {
    return Action.AC000;
}

ステートメント:

return Action.AC000;

いたるところに散らばっており、デフォルトの終点のようです。いくつかのケースでは、それをifステートメントの1つに含めるのが妥当と思われますが、代わりにデフォルトのreturnステートメントとして使用し、以下の多くを削除することを検討してください。

else {
    return Action.AC000;
}

これらのActionアイテムが少数のケースで重要な場合、ここで別のアクションを作成することは不可能です。次のステートメントを検討してください。

if(greaterThan(in.j,in.k)) {//110
    return Action.AC000;
}

次のいずれかになります。

if(greaterThan(in.j,in.k)) {//110
    return Action.AC123;
}

私の一番下の行は次のとおりです。

  • あなたが持っているコードは読みやすさのために大幅に改善することができます
  • 最初に、読みやすさの改善に取り組みます。
  • Ifステートメントの深さを減らすようにしてください
  • Action要素を一意に返すようにしてください。これにより、条件のすべての追跡を行う必要がなく、単にそれが返された場所を見つけることができます。
  • パスマトリックスは、上記の方法で実行できるクリーンなコードを作成できない場合にのみ使用してください。

いつものように、それは当面の問題に依存するので、パスを追跡することは本当にあなたの場合に行く方法であるかもしれませんが、もし私があなただったら私は他の場所に私の努力を最初に集中します。

追伸このセグメントで使用されているさまざまな書式設定スタイルは、実際に私のOCDをトリガーしています。

3
Robzor

1つのオプションは、ルールセットの実装を使用することです。基本的に、評価する必要があるすべての値のインターフェースを定義します。何かのようなもの:

interface Rule {
    boolean matches(Thing a, Thing b); // etc...
}

次に、シナリオごとにインスタンスを作成します。これは基本的に、提案したマトリックスソリューションと同等です。ネストは、各ルールがそれ自体に依存すると消えます。注文はおそらく重要であり、それはマイナス面になる可能性があります。

これが良い考えであるかどうかは、この混乱を定義するために来た実際のルールが何であるかを知っているか、それらに戻ることができるかによって異なります。大きな利点の1つは、DBにルールを配置したり、DSLを作成したり、実際のルールエンジンを作成したりするなど、よりダイナミックなソリューションに向いていることです。

2
JimmyJames

コードレビューであなたの質問に答えようとしていましたが、トピックから外れているのでそこで閉じられたため、この質問をここで追跡しました。私はRobzorと同じ疑念を抱いています。実際の問題は、ifツリーが長いことでも、ifツリーであるということでもありません。むしろ、問題は単にifツリーが恐ろしい混乱であることだと思います。「判読不能」であるというRobzorの説明は、私にはまったく誇張ではないようです。他の2つの回答はすでにコードに関する多くの問題を指摘していますが、たとえば、最初と同じように、いくつかの重複チェックがあるなど、まだいくつか追加する必要があります。

_if((
            equalz(tmp.b, "BA")
            && notEquals(in.c,"U")
            && equalz(in.d,"Y")
        )||(
            equalz(tmp.b, "HV")
            && notEquals(in.c,"U")
            && equalz(in.d,"Y")
            && varEqualsOneOf(in.e,"FLG","FLR","RRG"))
    )
_

ここで、notEquals(in.c,"U") && equalz(in.d,"Y")のチェックは、OR引数の両方に共通なので、外部に抽出できます。

また、_in.c_がない場合、notEquals(in.c,"U")は何を返すのでしょうか。 falseを返す場合、notEqualsは_!equalsOrMissing_と同等であり、2つのメソッドのいずれかが冗長であることを意味します。そして、それがtrueを返す場合、notEqualsは_!equalz_と同等であるため、1つのメソッドはどちらの方法でも冗長です。

これらの非人道的な怪物をもっと扱う必要がある場合、絶望と狂気の段階以外にそれらと一緒に行く唯一の方法は、実際に見ることができるようにifツリーをリファクタリングすることだと思います完全な恐怖から泣くことなく彼らを。私はあなたが提供したコードを使用してこれを実行しようとしましたが、実際にはスパゲッティコードのこのほぼ貫通できないファサードの背後にどのロジックが隠されているのかという印象を得るために、以下が得られました。

_@Override
public Action decide() {
    if (equalz(in.a, "LOC")) {
        if (notEquals(in.c, "U")
                && equalz(in.d, "Y")) {
            if (equalz(tmp.b, "BA")
                    && (varEqualsOneOf(in.e, "RRG", "NL")
                    && lessThan(in.f, in.g)
                    || varEqualsOneOf(in.e, "FLG", "FLR"))) {
                return Action.AC015;
            } else if (equalz(tmp.b, "HV")) {
                if (equalz(in.e, "RRG")) {
                    return Action.AC014;
                } else if (varEqualsOneOf(in.e, "FLG", "FLR")) {
                    return Action.AC010;
                }
            }
        } else if (!varEqualsOneOf(tmp.b, "BA", "HV")
                && equalsOrMissing(in.c, "U")
                && varEqualsOneOf(in.e, "FLG", "FLR")) {
            return Action.AC011;
        }
    } else if (varEqualsOneOf(in.a, "MNC", "BAN", "LCI", "CTV", "LEA", "INS")
            && !equalz(in.h, "A")
            && !equalz(in.i, "Y")
            && (notEquals(in.h, "U")
            && !greaterThan(in.j, in.k)
            || !notEquals(in.h, "U")
            && notEquals(in.l, "U")
            && varEqualsOneOf(in.e, "FLG", "FLR"))) {
        if (varEqualsOneOf(in.e, "FLG", "FLR", "RRG")
                || equalz(in.e, "NL")
                && equalz(in.a, "INS")) {
            if (!notEquals(in.l, "U")) {
                if (equalz(in.n, "Y")
                        || varEqualsOneOf(in.e, "FLG", "FLR")
                        && equalz(in.m, "Y")) {
                    return Action.AC012;
                } else {
                    return Action.AC002;
                }
            } else if (varEqualsOneOf(in.e, "FLG", "FLR")) {
                return Action.AC004;
            } else if (notEquals(in.o, "U")) {
                return Action.AC005;
            } else {
                return Action.AC018;
            }
        } else if (equalz(in.p, in.q)) {
            if (notEquals(in.o, "U")) {
                return Action.AC006;
            } else {
                return Action.AC008;
            }
        }
    }
    return Action.AC000;
}
_

これより簡単になるとは思いません。それはまだ複雑に見えますが、アルゴリズム自体は複雑に見え、それを回避する方法はありません。私も途中で間違いをしたかもしれないので、このコードがあなたのものと同等であると信頼しないでください。しかし、バグがないかどうかに関係なく、元のコードのこの簡略化されたバージョンは、ifツリーに不満があるか、または単に構造である場合、それが本当の性質であるかどうかを再考するための基礎を与える可能性がありますあなたの幸運な継承者である特定のifツリーの。また、コードを再構築することにした場合でも、ifツリーの多くのブランチのもつれを解くと、桁違いに簡単になります。

0
Stingy

データベースを検討します。

       {20,30,80,  
            (equalz(tmp.b, "BA")
            && notEquals(in.c,"U")
            && equalz(in.d,"Y")
            )||(
            equalz(tmp.b, "HV")
            && notEquals(in.c,"U")
            && equalz(in.d,"Y")
            && varEqualsOneOf(in.e,"FLG","FLR","RRG"))},

データについてはよくわかりませんが、これらのオブジェクトはほとんど変化しないようです。多分、

Action

action_id (auto-increment)
action
description


1, AC000, something
2, AC015, somethingelse

action_condition

action_condition_id (auto-increment)
condition_id
action_id
condition

1, 40, 2, RRG
2, 40, 2, NL
...

SELECT * FROM action a, action_condition ac WHERE a.action_id = ac.action_id AND ? = ?

//Build your SELECT, used Prepared Statements, 
//be careful constructing to disallow SQL Injection.

ただし、試す前にeverythingに名前を変更しました。

0
johnny