Uncle BobのClean Code原則に準拠するためにif-else ifステートメントのチェーンを編集するにはどうすればよいですか?


45

ボブおじさんのきれいなコードの提案に従い、特にメソッドを短くするようにしています。

ただし、このロジックを短縮することはできません。

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

私はelseを削除することができないため、全体を小さなビットに分割し、「else if」の「else」を使用するとパフォーマンスが向上します本当です、私はそれらを避けたいです。

意味的に言えば、前の条件が満たされた場合に次の条件を評価することは、ビジネスの観点からは意味がありません。


編集:この質問は、if(if else)elseを処理するエレガントな方法の可能な重複として識別されました。

これは別の質問だと思います(これらの質問の回答を比較することでも確認できます)。

  • 私の質問は、すぐに終わる最初の受け入れ条件をチェックしています。
  • リンクされた質問は、何かをするためにすべての条件を受け入れようとしている。(この質問に対するこの回答でよく見られますhttps : //softwareengineering.stackexchange.com/a/122625/96955

46
コンテキストでこのコードについて実際に間違っているまたは不明なものは何ですか?どのように短縮または簡略化できるかわかりません!条件を評価するコードは、決定の結果として呼び出されるメソッドと同様に、すでに十分に考慮されています。コードを複雑にするだけである、以下の回答のいくつかを見るだけです!
スティーブ

38
このコードには何の問題もありません。非常に読みやすく、簡単に理解できます。さらに縮小するために行うことは、間接性を追加し、理解しにくくします。

20
コードは問題ありません。残りのエネルギーを、それをさらに短くしようとするよりも生産的なものに入れます。
ロバートハーヴェイ

5
本当に4つの条件であれば、これで問題ありません。それが実際に12または50のようなものである場合は、おそらくこの1つの方法よりも高いレベルでリファクタリングする必要があります。
ジミージェームズ

9
コードはそのままにしておきます。両親がいつも言っていたことに耳を傾けてください。路上で子供たちにお菓子を提供しているおじさんを信用しないでください。@Harveyおもしろいことに、コードを「改善」しようとするさまざまな試みはすべて、コードをはるかに大きく、複雑にし、読みにくくしました。
gnasher729

回答:


81

理想的には、アラートコード/番号を独自のメソッドに取り込むためのロジックを抽出する必要があると思います。したがって、既存のコードは、

{
    addAlert(GetConditionCode());
}

GetConditionCode()で条件をチェックするためのロジックをカプセル化します。おそらく、マジックナンバーよりもEnumを使用した方が良いでしょう。

private AlertCode GetConditionCode() {
    if (CheckCondition1()) return AlertCode.OnFire;
    if (CheckCondition2()) return AlertCode.PlagueOfBees;
    if (CheckCondition3()) return AlertCode.Godzilla;
    if (CheckCondition4()) return AlertCode.ZombieSharkNado;
    return AlertCode.None;
}

2
あなたが説明するようにカプセル化できる場合(そうではないかもしれないと思う、OPは簡単にするために変数を省いていると思う)、それはコードを変更せず、それ自体は素晴らしいですが、コード人間工学と少しの読みやすさを追加します+ 1
opa

17
これらのアラートコードを使用すると、私は一つだけが一度に返すことができるコード感謝
ジョシュ・パート

12
これは、switchステートメントの使用にも最適です-OPの言語で使用できる場合。
フランクホプキンス

4
特定の状況に関する多くの情報を提供しなくても、複数の状況で役立つように記述できる場合は、新しいメソッドにエラーコードを取得することを抽出することをお勧めします。実際には、トレードオフとそれが価値がある場合の損益分岐点があります。しかし、多くの場合、検証のシーケンスは手元のジョブに固有のものであり、そのジョブと一緒に保持する方が良いことがわかります。そのような場合、新しいタイプを発明して、コードの別の部分に何をする必要があるかを伝えることは、望ましくないバラストです。
PJTraill

6
この再実装の1つの問題は、それが機能addAlertが偽の警告状態をチェックする必要にするということAlertCode.Noneです。
デビッドハンメン

69

重要な測定は、絶対サイズではなくコードの複雑さです。アクションが実際に示したものほど複雑ではないように、異なる条件は実際には単一の関数呼び出しであると仮定すると、コードには何も問題はありません。それは既に可能な限り単純です。

さらに「単純化」しようとすると、実際に事態は複雑になります。

もちろん、他の人が示唆elseしたreturnようにキーワードをに置き換えることができますが、それはスタイルの問題であり、複雑さの変化ではありません。


余談:

私の一般的なアドバイスは、きれいなコードのルールについて決して宗教的ではないことです:インターネット上で見るコーディングアドバイスのほとんどは、適切なコンテキストで適用されれば良いですが、どこにでも同じアドバイスを徹底的に適用すると、IOCCC。秘Theは常に、人間がコードについて簡単に推論できるようなバランスをとることです。

大きすぎる方法を使用してください。小さすぎる機能を使用すると、ねじが締められます。三項表現を避けてください、そしてあなたはめちゃくちゃです。どこでも三項式を使用してください、あなたはめちゃくちゃです。1行の関数を呼び出す場所と、50行の関数を呼び出す場所があることを認識します(はい、存在します!)。以下のための呼び出しという場所があることを実感if()文、およびのためのその呼び出し場所があることを?:オペレータが。あなたの自由に使える完全な武器を使用し、あなたが見つけることができる最も適切なツールを常に使用するようにしてください。そして、このアドバイスについても宗教的にならないでください。


2
私は、単純な(を削除する)が後に続くelse ifインナーで置き換えると、コードが読みにくくなると主張します。コードが言うとき、私はすぐに以前のものがなかった場合は、次のブロック内のコードのみがこれまでに実行されることを知っています。騒ぎも騒ぎもありません。プレーンである場合、前のものが実行されたかどうかに関係なく、実行される場合と実行されない場合があります。ここで、前のブロックを解析して最後がで終わることに注意するために、ある程度の精神的な努力を払わなければなりません。むしろ、ビジネスロジックの解析にその精神的な努力を費やしたいと思います。returnifelseelse ififreturn
CVn

1
それは小さなことですが、少なくとも私にとっては、else if1つの意味単位を形成します。(コンパイラーにとって必ずしも単一のユニットであるとは...; return; } if (...限りませんが、それは大丈夫です。)そうではありません。それが複数行に広がっている場合はもちろんです。それは、キーワードペアを見るだけで直接取り込むことができるのではなく、実際にそれが何をしているのかを見るために見なければならないものelse ifです。
からCVn

@MichaelKjörlingFull Ack。私はelse if、特にチェーン形式がそのようなよく知られたパターンであるので、自分自身がコンストラクトを好むでしょう。ただし、フォームのコードif(...) return ...;もよく知られているパターンなので、完全に非難するつもりはありません。しかし、私はこれを小さな問題と考えています。制御フローロジックはどちらの場合も同じであり、if(...) { ...; return; }はしごをよく見ると、実際にはelse ifはしごと同等であることがわかります。単一の用語の構造を見て、その意味を推測し、それがどこでも繰り返されていることに気付き、何が起こっているかを知っています。
cmaster

JavaScript / node.jsからは else if との 両方使用する「ベルトとサスペンダー」コードを使用する人もいreturnます。例else if(...) { return alert();}
user949300

1
「そして、このアドバイスについても宗教的にならないでください。」+1
ジャレッドのような言葉

22

これは、特定のケースで、通常のif..elseよりも「良い」かどうかについて議論の余地があります。しかし、他の何かを試したい場合は、これが一般的な方法です。

条件をオブジェクトに入れ、それらのオブジェクトをリストに入れます

foreach(var condition in Conditions.OrderBy(i=>i.OrderToRunIn))
{
    if(condition.EvaluatesToTrue())
    {
        addAlert(condition.Alert);
        break;
    }
}

条件に応じて複数のアクションが必要な場合は、おかしな再帰を実行できます

void RunConditionalAction(ConditionalActionSet conditions)
{
    foreach(var condition in conditions.OrderBy(i=>i.OrderToRunIn))
    {
        if(condition.EvaluatesToTrue())
        {
            RunConditionalAction(condition);
            break;
        }
    }
}

明らかにそうです。これは、ロジックにパターンがある場合にのみ機能します。超汎用の再帰的条件付きアクションを作成しようとすると、オブジェクトのセットアップは元のifステートメントと同じくらい複雑になります。独自の新しい言語/フレームワークを発明します。

しかし、あなたの例にパターンがあります

このパターンの一般的な使用例は、検証です。の代わりに :

bool IsValid()
{
    if(condition1 == false)
    {
        throw new ValidationException("condition1 is wrong!");
    }
    elseif(condition2 == false)
    {
    ....

}

になる

[MustHaveCondition1]
[MustHaveCondition2]
public myObject()
{
    [MustMatchRegExCondition("xyz")]
    public string myProperty {get;set;}
    public bool IsValid()
    {
        conditions = getConditionsFromReflection()
        //loop through conditions
    }
}

27
これは、if...elseラダーをConditionsリストの構築に移動するだけです。の構築にConditionsはOPコードと同じだけのコードが必要になるため、正味のゲインはマイナスになりますが、間接性を追加すると読みやすさが犠牲になります。私は間違いなくきれいなコード化されたラダーを好むでしょう。
cmaster

3
@cmaster yesはい、私は「オブジェクトのセットアップは元のifステートメントと同じくらい複雑になるでしょう」と正確に言ったと思います£
ユアン

7
これはオリジナルよりも読みにくいです。実際にチェックされている状態を把握するには、コードの他の領域を掘り下げる必要があります。不要なレベルの間接参照が追加され、コードが理解しにくくなります。

8
if .. else if .. else ..チェーンを述語とアクションのテーブルに変換するのは理にかなっていますが、それはもっと大きな例に限られます。この表は複雑さと間接性を追加するため、この概念上のオーバーヘッドを償却するのに十分なエントリが必要です。したがって、4つの述語/アクションのペアでは、単純な元のコードを保持しますが、100を持っている場合は、間違いなくテーブルを使用します。クロスオーバーポイントは中間にあります。@cmaster、テーブルは静的に初期化できるため、述語/アクションのペアを追加するための増分オーバーヘッドは、それらを単に名前を付ける1行です。
スティーブンC.スチール

2
可読性は個人的なものではありません。それはプログラミングの大衆に対する義務です。主観的です。これがまさにこのような場所に来て、それについてプログラミングの大衆が言わなければならないことを聞くことが重要である理由です。個人的には、この例は不完全だと思います。conditions構成方法を教えてください... ARG!アノテーション属性ではありません!なぜ神?目が痛い!
candied_orange

7

return;1つの条件が成功した後に使用することを検討してくださいelsereturn addAlert(1)そのメソッドに戻り値がある場合は、直接できる場合もあります。


3
もちろん、これはifs のチェーンの後に他に何も起こらないことを前提としています...それは合理的な仮定かもしれませんし、そうではないかもしれません。
CVn

5

私は時々、このような構造がクリーナーと見なされるのを見てきました:

switch(true) {
    case cond1(): 
        statement1; break;
    case cond2():
        statement2; break;
    case cond3():
        statement3; break;
    // .. etc
}

正しい間隔の3進法も適切な選択肢です。

cond1() ? statement1 :
cond2() ? statement2 :
cond3() ? statement3 : (null);

条件と関数を含むペアで配列を作成し、最初の条件が満たされるまで繰り返し処理を試みることもできると思います。


1
三元系はきちんとしている
ユアン

6
@Ewanが壊れた「深く再帰的な3項」をデバッグするのは、不必要な痛みです。
dfri

5
ただし、画面上できれいに見えます。
ユアン

ええと、どの言語でcaseラベル付きの関数を使用できますか?
アンダーキャット

1
有効なECMAScript / JavaScript afaikである@undercat
zworek

1

@Ewanの回答の変形として、次のような条件のチェーン(「フラットリスト」の代わりに)を作成できます。

abstract class Condition {
  private static final  Condition LAST = new Condition(){
     public void alertOrPropagate(DisplayInterface display){
        // do nothing;
     }
  }
  private Condition next = Last;

  public Condition setNext(Condition next){
    this.next = next;
    return this; // fluent API
  }

  public void alertOrPropagate(DisplayInterface display){
     if(isConditionMeet()){
         display.alert(getMessage());
     } else {
       next.alertOrPropagate(display);
     }
  }
  protected abstract boolean isConditionMeet();
  protected abstract String getMessage();  
}

この方法では、定義された順序で条件を適用でき、インフラストラクチャ(示されている抽象クラス)は、最初のチェックが満たされた後、残りのチェックをスキップします。

これは、条件を適用するループで「スキップ」を実装する必要がある「フラットリスト」アプローチよりも優れている場所です。

条件チェーンを設定するだけです:

Condition c1 = new Condition1().setNext(
  new Condition2().setNext(
   new Condition3()
 )
);

そして、簡単な呼び出しで評価を開始します。

c1.alertOrPropagate(display);


4
私は他の誰かに代弁するふりはしませんが、問題のコードはすぐに読みやすく、その動作は明らかですが、それが何をするのかすぐに明らかになるとは思いません
からCVn

0

まず、元のコードはひどいIMOではありません。それはかなり理解可能であり、本質的に悪いことは何もありません。

それからあなたがそれを嫌うなら、リストを使うが彼のやや不自然なforeach breakパターンを取り除くという@Ewanのアイデアに基づいて:

public class conditions
{
    private List<Condition> cList;
    private int position;

    public Condition Head
    {
        get { return cList[position];}
    }

    public bool Next()
    {
        return (position++ < cList.Count);
    }
}


while not conditions.head.check() {
  conditions.next()
}
conditions.head.alert()

選択した言語でこれを調整し、リストの各要素をオブジェクト、タプル、その他のものにします。

編集:私が思ったほど明確ではないように見えるので、さらに説明させてください。conditionsある種の順序付きリストです。head調査対象の現在の要素です。最初はリストの最初の要素であり、next()呼び出されるたびに次の要素になります。check()そして、OPからalert()checkConditionX()and addAlert(X)です。


1
(降格しませんでしたが)私はこれに従うことができません。とは?
ベルソフィー

@Belle答えを編集してさらに説明しました。これは、Ewanのアイデアと同じですが、のwhile not代わりにを使用していforeach breakます。
ニコ

素晴らしいアイデアの素晴らしい進化
ユアン

0

質問にはいくつかの詳細が欠けています。条件が次の場合:

  • 変更または
  • アプリケーションまたはシステムの他の部分で繰り返される、または
  • 特定の場合に変更(異なるビルド、テスト、展開など)

またはのコンテンツaddAlertがより複雑な場合、たとえばc#のより良い解決策は次のようになります。

//in some central spot
IEnumerable<Tuple<Func<bool>, int>> Conditions = new ... {
  Tuple.Create(CheckCondition1, 1),
  Tuple.Create(CheckCondition2, 2),
  ...
}

//at the original place
var matchingCondition = Conditions.Where(c=>c.Item1()).FirstOrDefault();
if(matchingCondition != null) 
  addAlert(matchingCondition.Item2)

タプルはc#<8ではそれほど美しくありませんが、利便性のために選択されています。

この方法の長所は、上記のオプションが適用されない場合でも、構造体が静的に型付けされることです。たとえば、を紛失して誤って台無しにすることはできませんelse


0

たくさんある場合にCyclomaticの複雑さを軽減する最良の方法if->then statementsは、辞書またはリスト(言語依存)を使用してキー値(ステートメント値またはその値の場合)と値/関数の結果を保存することです。

たとえば、(C#)の代わりに:

if (i > 10) { return "Two"; }
else if (i > 8) { return "Four" }
else if (i > 4) { return "Eight" }
return "Ten";  //etc etc say anything after 3 or 4 values

簡単にできる

var results = new Dictionary<int, string>
{
  { 10, "Two" },
  { 8, "Four"},
  { 4, "Eight"},
  { 0, "Ten"},
}

foreach(var key in results.Keys)
{
  if (i > results[key]) return results.Values[key];
}

より新しい言語を使用している場合は、より多くのロジックを格納でき、単純に値も格納できます(c#)。これは実際には単なるインライン関数ですが、ロジックがインラインに配置するのが面倒な場合は、他の関数を指すこともできます。

var results = new Dictionary<Func<int, bool>, Func<int, string>>
{
  { (i) => return i > 10; ,
    (i) => return i.ToString() },
  // etc
};

foreach(var key in results.Keys)
{ 
  if (key(i)) return results.Values[key](i);
}

0

ボブおじさんのきれいなコードの提案に従い、特にメソッドを短くするようにしています。

ただし、このロジックを短縮することはできません。

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

コードはすでに短すぎますが、ロジック自体は変更しないでください。一見するとcheckCondition()、の呼び出しが4回繰り返されているように見えますが、コードを注意深く読み直した後はそれぞれが異なることが明らかです。適切なフォーマットと関数名を追加する必要があります。例:

if (is_an_apple()) {
  addAlert(1);
}
else if (is_a_banana()) {
  addAlert(2);
}
else if (is_a_cat()) {
  addAlert(3);
}
else if (is_a_dog()) {
  addAlert(4);
}

あなたのコードは何よりも読みやすいものでなければなりません。ボブおじさんの本を何冊か読んだことがあるが、それは彼が一貫して伝えようとしているメッセージだと思う。


0

すべての機能が同じコンポーネントに実装されていると仮定すると、フロー内の複数のブランチを取り除くために、機能に何らかの状態を保持させることができます。

EG:前の条件が満たされたかどうかをチェックcheckCondition1()するになりevaluateCondition1()ます。もしそうなら、それによって取得されるいくつかの値をキャッシュしますgetConditionNumber()

checkCondition2()になりevaluateCondition2()、前の条件が満たされているかどうかを確認します。前の条件が満たされなかった場合、条件シナリオ2をチェックし、取得する値をキャッシュしますgetConditionNumber()。等々。

clearConditions();
evaluateCondition1();
evaluateCondition2();
evaluateCondition3();
evaluateCondition4();
if (anyCondition()) { addAlert(getConditionNumber()); }

編集:

このアプローチが機能するために、高価な条件のチェックを実装する必要がある方法を次に示します。

bool evaluateCondition34() {
    if (!anyCondition() && A && B && C) {
        conditionNumber = 5693;
        return true;
    }
    return false;
}

...

bool evaluateCondition76() {
    if (!anyCondition() && !B && C && D) {
        conditionNumber = 7658;
        return true;
    }
    return false;
}

したがって、実行する高価なチェックが多すぎて、このコードの内容がプライベートのままである場合、このアプローチはそれを維持するのに役立ち、必要に応じてチェックの順序を変更できます。

clearConditions();
evaluateCondition10();
evaluateCondition9();
evaluateCondition8();
evaluateCondition7();
...
evaluateCondition34();
...
evaluateCondition76();

if (anyCondition()) { addAlert(getConditionNumber()); }

この回答は、他の回答からいくつかの代替提案を提供するだけであり、おそらく4行のコードのみを考慮した場合、元のコードよりも良くなることはありません。私が言及したシナリオを考えると、これはひどいアプローチではありません(また、他の人が言ったようにメンテナンスを難しくすることもありません)


私はこの提案が好きではありません-それは複数の関数内のテストロジックを隠します。これにより、たとえば、順序を変更し、#2の前に#3を実行する必要がある場合、コードの保守が難しくなります。
ローレンス

いいえanyCondition() != false。の場合、以前の条件が評価されたかどうかを確認できます。
エマーソンカルドーソ

1
わかりました、あなたが何を得ているかわかります。ただし、条件2と条件3の両方がに評価されるtrue場合、OPは条件3を評価することを望みません。
ローレンス

私が意味したのは、anyCondition() != false関数内でチェックできるということですevaluateConditionXX()。これは実装可能です。内部状態を使用するアプローチが望ましくない場合、私は理解しますが、これが機能しないという議論は無効です。
エマーソンカルドーソ

1
はい、私の反対は、テストロジックを役に立たないように隠すことであり、機能しないことではありません。回答(段落3)で、条件1を満たすためのチェックがeval ... 2()内に配置されます。しかし、もし彼がトップレベルで条件1と2を切り替えた場合(顧客の要件の変更などにより)、条件1のチェックを削除してevalに入るためにeval ... 2()に行く必要があります。 ..1()を使用して、条件2のチェックを追加します。これを機能させることはできますが、メンテナンスの問題を簡単に引き起こす可能性があります。
ローレンス

0

「else」句が2つを超えると、コードの読者はチェーン全体を調べて目的のコードを見つけます。次のようなメソッドを使用します。void AlertUponCondition(Condition condition){switch(condition){case Condition.Con1:... break; case Condition.Con2:... break; など...}「条件」は適切な列挙型です。必要に応じて、ブールまたは値を返します。次のように呼び出します:AlertOnCondition(GetCondition());

それは本当に簡単なものではありませんし、いくつかのケースを超えるとif-elseチェーンよりも高速になります。


0

コードが具体的ではないため、特定の状況について話すことはできませんが、...

そのようなコードは、多くの場合、オブジェクト指向モデルが欠けているためにおいです。本当に4つのタイプがあり、それぞれが独自のアラータタイプに関連付けられていますが、これらのエンティティを認識してそれぞれのクラスインスタンスを作成するのではなく、それらを1つのものとして扱い、後でそれを補おうとします。先に進むには何を扱っているかを知る必要があります。

多態性がより適している可能性があります。

長いまたは複雑なif-thenコンストラクトを含む長いメソッドを使用したコードには疑いを持ちます。多くの場合、いくつかの仮想メソッドを備えたクラスツリーが必要です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.