重构if-else if - else

时间:2013-09-11 18:16:56

标签: c# design-patterns if-statement refactoring solid-principles

我有以下代码示例

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种不同的“费率”,可以根据您在下面看到的时间使用。我考虑过为每个速率创建一个策略实现,但后来我会移动逻辑来确定使用的策略并使其变得混乱。

谢谢!

9 个答案:

答案 0 :(得分:18)

如果您真的在寻找设计模式,我会选择责任链模式。

基本上你的“链接”试图处理输入。如果它无法处理它,它会传递到链中,直到另一个链接可以处理它。如果你有单独的测试,你也可以在单元测试中定义一个易于模拟的界面。

所以你有一个每个链接都会继承的抽象类:

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

所有你需要做的就是通过一个干净的电话呼叫链:

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

答案 1 :(得分:8)

有一种名不见经传的模式称为“规则模式

我们的想法是将所有内容提取到对象中让它处理自己的工作。您将定义哪个每个规则的每个类,这是您的条件声明,例如: (object.Time&gt; 0&amp;&amp;&lt; = 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);
    }
}

这里的想法是,一旦你在条件下嵌套,就会更难阅读条件语句,开发人员很难做出任何改变。因此,它将每个单独规则的逻辑及其效果分为遵循规则单一责任模式的自己的类。

有些注意事项 1.)只读 2.)明确的顺序 3.)依赖性 4.)优先权 5.)坚持

同样,如果您的条件复杂性越来越多,并考虑到应用程序的要求,请考虑使用规则模式。

如果您不希望它返回小数或其他内容,您可以自定义它,但想法就在这里。

答案 2 :(得分:5)

您只需要检查范围的一个端点。另一个是由你实际存在于代码中的那一点暗示的,因为早期的条件是错误的。

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

出于可读性和性能原因,最好将比例尺更常见的一端放在首先检查的那一端。但无论哪种方式都可以。

答案 3 :(得分:2)

您可以在if-else条件中使用格式而不是设计模式;

一般情况下,如果你lot of conditions我比if更喜欢嵌套if-else,你可以选择这样的东西;

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

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

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

答案 4 :(得分: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;

答案 5 :(得分:1)

我真的很喜欢Leo Lorenzo Luis的解决方案。 但是,我不会回复率,而是让规则对它做些什么。 这将尊重S.O.L.I.D. Principles中的S. 和Law Of Demeter

此外,当一个班级&#34;要求&#34;对于包含在另一个类中的值,您可以将其标识为smell,称为data class。你应该尽量避免这种情况。

话虽如此:我会做两件事来改善Leo Lorenzo的解决方案:

  1. 在没有Rule的情况下调用for loop
  2. 执行其关联的Rule类内所请求的行为。
  3. 为了做到这一点,我们必须将rule类映射到它们的时间范围,这样就可以直接访问它们而不必遍历循环。您需要实现自己的地图对象(或列表对象或集合),重载[] operator及其add函数

    因此您可以在地图中添加规则,例如:

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

    您可以在上面看到,有一个对象Range可以与其关联的对象Rule。因此,您最终可以为同一Range添加多个规则。

    然后,你这样称呼它:

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

答案 6 :(得分:1)

使用地图:

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&#39; 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值。此外,添加一个defwast规则,alwasys评估true并返回零。然后更改实现(测试)以获取第一个规则的值来评估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; }
    }
}

答案 7 :(得分:0)

我认为这不是反模式问题,而且代码指标也是oki。 如果没有嵌套而且不是很复杂! 但是你可能会做得更好,比如使用 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%从外面测试!

答案 8 :(得分:0)

如果您有大量的“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;

您可以始终拥有可为空的值,或将-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;