19

我有以下代码示例

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 种不同的“比率”,可以根据您在下面看到的时间使用。我曾考虑为每个汇率创建一个策略实施,但随后我将移动确定要使用的策略的逻辑,并将其弄得一团糟。

谢谢!

4

9 回答 9

33

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

基本上你的“链接”试图处理输入。如果它无法处理它,它会沿着链条传递,直到另一个链接可以处理它。如果有的话,您还可以定义一个接口以便在单元测试中轻松模拟。

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

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);
于 2013-09-11T19:30:45.687 回答
13

有一种不那么出名的模式叫做“规则模式

这个想法是将所有内容提取到一个对象中让它处理自己的工作。您将拥有您定义的每个规则的每个类,这是您的条件语句,例如(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.) 持久性

同样,当您的条件复杂性越来越多并且您的应用程序的要求需要保证时,请考虑使用规则模式。

如果您不想让它返回小数或其他东西,您可以自定义它,但想法就在这里。

于 2013-09-11T18:36:21.300 回答
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;
}

出于可读性和性能的原因,最好将更常见的一端作为您首先检查的一端。但它会以任何一种方式工作。

于 2013-09-11T18:41:58.903 回答
2

Using a map:

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

I like the idea of @lll's Rules Pattern answer but it has a flaw.

Consider the following test (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));
}

The test fails. Although RuleNumberOne was called and returned a non-zero value, RuleNumberTwo was subsequently called and returned zero to overwrite the correct value.

In order to replicate the if..else..else logic, it need to be able to short circuit.

Here's a quick fix: change the interface's Execute method to return a bool to indicate whether to rule should fire and add a Value property to get the rule's decimal value. Also, add a defulat rule that alwasys evaluates true and returns zero. Then change the implementation (test) to get the value of the first rule to evaluate 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; }
    }
}
于 2014-06-20T10:10:03.977 回答
2

您可以在if-else条件下选择一种格式而不是设计模式;

一般来说,如果你有lot of conditionsif比很多嵌套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.
于 2013-09-11T18:20:17.223 回答
1

我真的很喜欢 Leo Lorenzo Luis 的解决方案。但是,我不会返回 Rate,而是让 Rule 对其进行处理。这将尊重来自SOLID 原则 和得墨忒耳定律的 S .。

此外,当一个类“请求”一个包含在另一个类中的值时,您可以将其标识为smell称为数据类。你应该尽量避免这种情况。

话虽这么说:我会做两件事来完善 Leo Lorenzo 的解决方案:

  1. 调用Rule不带for loop.
  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);
于 2013-09-12T02:30:55.477 回答
0

Just do one comparison in each if, and go top-to-bottom with the values:

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;
于 2013-09-11T18:39:46.723 回答
0

我不认为这是一个反模式问题,代码指标也可以。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% 的测试!

于 2013-09-11T18:26:33.157 回答
0

如果您有大量的“如果”,或者您想将此信息放入设置文件中,那么我建议您创建一个类来存储此信息。

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;
于 2013-09-11T18:36:31.660 回答