web-dev-qa-db-ja.com

if-elseif-elseのリファクタリング

次のコード例があります

if(object.Time > 0 && <= 499)
{
     rate = .75m
}
else if(object.Time >= 500 && <= 999)
{
     rate = .85m
}
else if(object.Time >= 1000)
{
     rate = 1.00m
}
else
{
     rate = 0m;
}

私の質問は、これを改善するためにどのデザインパターンを使用できるかということです。

編集:もう少し明確にするために、ここに表示されるコードは、戦略パターンの実装内に現在存在するものです。 3種類の計算があり、そのうち2つには、以下に示す時間に基づいて使用できる3つの異なる「レート」があります。レートごとに戦略の実装を作成することを考えましたが、使用する戦略を決定するためのロジックを移動し、それも混乱させます。

ありがとう!

14
Alex

あなたが本当にデザインパターンを探しているなら、私はChainofResponsibilityパターンを選びます。

基本的に、「リンク」は入力を処理しようとします。処理できない場合は、他のリンクが処理できるようになるまでチェーンに渡されます。ユニットテストがある場合は、ユニットテストで簡単にモックするためのインターフェイスを定義することもできます。

したがって、すべてのリンクが継承するこの抽象クラスがあります。

public abstract class Link
{
    private Link nextLink;

    public void SetSuccessor(Link next)
    {
        nextLink = next;
    }

    public virtual decimal Execute(int time)
    {
        if (nextLink != null)
        {
            return nextLink.Execute(time);
        }
        return 0;
    }
}

次に、ルールを使用して各リンクを作成します。

public class FirstLink : Link
{
    public override decimal Execute(int time)
    {
        if (time > 0 && time <= 499)
        {
            return .75m;
        }

        return base.Execute(time);
    }
}

public class SecondLink : Link
{
    public override decimal Execute(int time)
    {
        if (time > 500 && time <= 999)
        {
            return .85m;
        }

        return base.Execute(time);
    }
}

public class ThirdLink : Link
{
    public override decimal Execute(int time)
    {
        if (time >= 1000)
        {
            return 1.00m;
        }

        return base.Execute(time);
    }
}

最後に、それを使用するには、すべての後継者を設定して呼び出します。

Link chain = new FirstLink();
Link secondLink = new SecondLink();
Link thirdLink = new ThirdLink();


chain.SetSuccessor(secondLink);
secondLink.SetSuccessor(thirdLink);

そして、あなたがしなければならないのは、1回のクリーンコールでチェーンを呼び出すことだけです。

var result = chain.Execute(object.Time);
19

'ルールパターン'というあまり有名ではないパターンがあります

すべてがオブジェクトに抽出されるおよび独自のジョブを処理するという考え方です。 各ルールの各クラス定義した条件ステートメントがあります。例: (object.Time> 0 && <= 499)

public class RuleNumberOne : IRules
{
   public decimal Execute(Oobject date)
   {
      if(date.Time > 0 && date.Something <= 499)
         return .75m;
      return 0;
   } 
} 

public class RuleNumberTwo : IRules
{
    public decimal Execute(Oobject date)
    {
        if(date.Time >= 500 && date.Something <= 999)
            return .85m;
        return 0;
    } 
} 

public interface IRules
{ 
  decimal Execute(Oobject date);
}

したがって、以前はこのように見えていたクラスでは

if(object.Time > 0 && <= 499)
{
     rate = .75m
}
else if(object.Time >= 500 && <= 999)
{
     rate = .85m
}
else if(object.Time >= 1000)
{
     rate = 1.00m
}
else
{
     rate = 0m;
}

なります、

private List<IRules>_rules = new List<IRules>();
public SomeConstructor()
{
    _rules.Add(new RuleNumberOne());
    _rules.Add(new RuleNumberTwo());
}

public void DoSomething()
{

    Oobject date = new Oobject();

    foreach(var rule in this._rules)
    {
        Decimal rate = rule.Execute(date);
    }
}

ここでの考え方は、if条件をネストすると、条件ステートメントを読むのが難しくなり、開発者が変更を加えるのが難しくなるということです。したがって、個々のルールのロジックとその効果を、ルールの単一責任パターンに従う独自のクラスに分離します。

いくつかの考慮事項は 1.)読み取り専用2.)明示的な順序3.)依存関係4.)優先度5.)永続性

繰り返しになりますが、条件の複雑さが増し、アプリケーションの要件がそれを保証する場合は、ルールパターンの使用を検討してください。

小数などを返したくない場合はカスタマイズできますが、アイデアはここにあります。

9
123 456 789 0

範囲の1つのエンドポイントのみをチェックする必要があります。もう1つは、以前の条件が偽だったため、コード内のその時点に実際にいることを意味します。

if (obj.Time <= 0) {
    rate = 0.00m;
}

// At this point, obj.Time already must be >= 0, because the test
// to see if it was <= 0 returned false.
else if (obj.Time < 500) {
    rate = 0.75m;
}

// And at this point, obj.Time already must be >= 500.
else if (obj.Time < 1000) { 
    rate = 0.85m;
}

else {
    rate = 1.00m;
}

読みやすさとパフォーマンスの理由から、スケールのより一般的な端を最初にチェックするものにすることをお勧めします。しかし、どちらの方法でも機能します。

5
cHao

if-else条件では、デザインパターンではなくフォーマットを選択できます。

一般に、lot of conditionsがある場合はネストされたif-elseよりもifの方が好きです。次のようなものを選ぶことができます。

if(condition1){
   return x;    // or some operation
}

if(condition 2){
   return y;   // or some operation
}

return default; // if none of the case is satisfied.
2
JNL

私はLeoLorenzoLuisのソリューションが本当に好きです。しかし、レートを返す代わりに、ルールに何かをさせます。これは、 S.O.L.I.D。Principles および Law Of Demeter からのSを尊重します。

また、クラスが別のクラスに含まれている値を「要求」する場合、それを データクラス と呼ばれるsmellとして識別できます。これは避けてください。

そうは言っても、私はLeoLorenzoのソリューションを磨くために2つのことをします。

  1. for loopなしで右のRuleを呼び出します。
  2. 関連付けられたRuleクラス内で要求された動作を実行します。

これを行うには、ruleクラスを時間範囲にマップする必要があります。これにより、ループを繰り返す代わりに、直接アクセスできるようになります。独自のマップオブジェクト(またはリストオブジェクト、またはコレクション)を実装し、[] operatorとそれがadd関数をオーバーロードする必要があります。

たとえば、次のようにマップにルールを追加できます。

ranges.Add(0,500).AddRule(rule1);
ranges.Add(500,1000).AddRule(rule2);
etc.. 

上記のように、オブジェクトRangeを関連付けることができるオブジェクトRuleがあります。したがって、最終的には同じRangeに複数のルールを追加できます。

次に、次のように呼び出します。

ranges[Object.time].Execute(Object);
1
Simon

マップの使用:

var map = new[]
{
    new { Rule = (Func<Oobject, bool>) ( x => x.Time > 0 && x.Something <= 499 ), 
          Value = .75m },
    new { Rule = (Func<Oobject, bool>) ( x => x.Time >= 500 && x.Something <= 999 ), 
          Value = .85m },
    new { Rule = (Func<Oobject, bool>) ( x => true ), 
          Value = 0m }
};

var date = new Oobject { Time = 1, Something = 1 };
var rate = map.First(x => x.Rule(date) ).Value;

Assert.That( rate, Is.EqualTo(.75m));

@lllのRules Pattern回答のアイデアは気に入っていますが、欠点があります。

次のテスト(NUnit)について考えてみます。

[Test]
public void TestRulesUsingList()
    var rules = new IRules[]{ new RuleNumberOne(), new RuleNumberTwo() };

    var date = new Oobject { Time = 1, Something = 1 };
    var rate = 0m;

    foreach(var rule in rules)
        rate = rule.Execute(date);

    Assert.That( rate, Is.EqualTo(.75m));
}

テストは失敗します。 RuleNumberOneが呼び出されてゼロ以外の値が返されましたが、その後RuleNumberTwoが呼び出されてゼロが返され、正しい値が上書きされました。

If..else..elseロジックを複製するには、短絡できる必要があります。

簡単な修正方法は次のとおりです。インターフェイスのExecuteメソッドを変更してboolを返し、ルールを実行するかどうかを示し、Valueプロパティを追加してルールのdecimal値を取得します。また、alwasysがtrueを評価し、ゼロを返すdefulatルールを追加します。次に、実装(テスト)を変更して、trueと評価する最初のルールの値を取得します。

[Test]
public void TestRulesUsingList2()
{
    var rules = new IRules[]{ new RuleNumberOne(), new RuleNumberTwo(), 
        new DefaultRule() };

    var date = new Oobject { Time = 1, Something = 1 };
    var rate = rules.First(x => x.Execute(date)).Value;

    Assert.That( rate, Is.EqualTo(.75m));
}

public class Oobject
{
    public int Time { get; set; }
    public int Something { get; set; }
}

public interface IRules
{ 
    bool Execute(Oobject date);
    decimal Value { get; }
}

public class RuleNumberOne : IRules
{
    public bool Execute(Oobject date)
    {
        return date.Time > 0 && date.Something <= 499;
    }

    public decimal Value
    {
        get { return .75m; }
    }
} 

public class RuleNumberTwo : IRules
{
    public bool Execute(Oobject date)
    {
        return date.Time >= 500 && date.Something <= 999;
    }

    public decimal Value
    {
        get { return .85m; }
    }
} 

public class DefaultRule : IRules
{
    public bool Execute(Oobject date)
    {
        return true;
    }

    public decimal Value
    {
        get { return 0; }
    }
}
1
onedaywhen

大量の「if」がある場合、またはこの情報を設定ファイルに入れたい場合は、この情報を格納するクラスを作成することをお勧めします。

Class
    FromTime
    ToTime
    Value

values.Add(New Class(0, 499, .75));
values.Add(New Class(500, 999, .85));
values.Add(New Class(1000, 9999, 1));

次に、コレクション内の各アイテムをループします

if(object.Time >= curItem.FromTime && object.Time <= curItem.ToTime)
    rate = curItem.Value;

常にnull許容値を設定するか、-1を無限大として設定できます。

values.Add(New Class(-1, 0, 0));
values.Add(New Class(0, 499, .75));
values.Add(New Class(500, 999, .85));
values.Add(New Class(1000, -1, 1));

if((object.Time >= curItem.FromTime || curItem.FromTime == -1) && (object.Time <= curItem.ToTime || curItem.ToTime == -1))
    rate = curItem.Value;
0
the_lotus

Ifごとに1つの比較を行い、値を上から下に移動します。

if (Object.Time >= 1000)
    rate = 1.0;
else
    if (Object.Time >= 500)
        rate = 0.85;
    else
        if (Object.Time > 0)
            rate = 0.75;
        else
            rate = 0;
0
qwerty13579

これはアンチパターンの問題ではないと思います。コードメトリクスの場合も問題ありません。 Ifはネストされておらず、それほど複雑ではありません!。ただし、たとえばSwitchを使用して改善したり、IsAgeBiggerThanMax()などのプロパティを含む独自のクラスを作成したりできます。


スイッチの更新:

        var range = (time - 1) / 499;
        switch (range)
        {
            case 0:  // 1..499
                rate = 0.75;
                break;
            case 1: // 500.. 999 
                rate = 0.85;
                break;
            default:
                rate = 0;
                if (time == 1000)
                {
                    rate = 1.0;
                }
                break;
        }

テストは哲学の質問であり、この機能が何で、何が行われているのかわかりません。多分それは外部から100%テストすることができます!

0
Bassam Alugili