Рефакторинг if-else if-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;
}

мой вопрос в том, что шаблон я могу использовать, чтобы сделать это лучше?

Edit: просто чтобы уточнить немного лучше, код, который вы видите здесь, - это то, что в настоящее время существует в реализации шаблона стратегии. У нас есть 3 типа расчетов, 2 из которых имеет 3 разных "ставки", которые могут быть использованы на основе времени, которое вы видите ниже. Я думал о создании стратегии реализации для каждой ставки, но потом я было бы перемещение логики для определения стратегии для использования и сделать это беспорядок, а также.

спасибо!

9 ответов


если вы действительно ищете шаблон дизайна, я бы пошел на шаблон цепочки ответственности.

в основном ваша "ссылка"пытается обрабатывать ввод. Если он не может справиться с этим, он передается вниз по цепочке, пока другое звено не сможет справиться с ним. Вы также можете определить интерфейс для легкой насмешки в модульных тестах, если у вас есть.

Итак, у вас есть этот абстрактный класс, который унаследует каждая ссылка:

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

существует не столь известный шаблон под названием' Правила Pattern'

идея в том, что все извлекается в объект и пусть он справляется со своей работой. У вас будет каждый класс для каждого правила что вы определили, что является вашим условием, например (object.Время > 0 &&

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, было бы сложнее читать операторы condition и его трудно для разработчика внести какие-либо изменения. Таким образом, она разделяет логику каждого отдельного правила и его действие на свой собственный класс, который следует правилу единой модели ответственности.

некоторые соображения 1.) Только для чтения 2.) Явный порядок 3.) Зависимости 4.) Приоритет 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;
}

было бы лучше сделать более общий конец шкалы тем, который вы сначала проверяете, по причинам читаемости и производительности. Но это сработает в любом случае.


вы можете пойти на формат, а не шаблон дизайна в if-else условия;

вообще, если у вас есть lot of conditions предпочитаю if чем много вложенных if-else's Вы можете выбрать что-то вроде этого;

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

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

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

просто сделайте одно сравнение в каждом if и перейдите сверху вниз со значениями:

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;

мне очень нравится решение Лео Лоренцо Луиса. Но вместо того, чтобы вернуть ставку, я бы позволил правилу что-то сделать с ней. Это будет уважать S. От С. О. Л. И. Д. Принципы и Закон Деметры.

кроме того, когда класс "запрашивает" значение, содержащееся в другом классе, вы можете определить его как smell назвал класса. Вы должны попытаться избежать этого.

это, как говорится: я бы сделал две вещи, чтобы отполировать решение Лео Лоренцо:

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

Если у вас есть огромное количество "Если" или если вы хотите поместить эту информацию в файл настроек, я бы предложил вам создать класс для хранения этой информации.

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;

вы всегда можете иметь значения nullable или установить -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;

Я не думаю, что это проблема с анти-шаблоном, и для метрик кода также oki. The Если не является вложенным и не очень сложным!. Но вы могли бы сделать лучше, например, используя переключатель или вы создаете собственный класс, который содержит свойства 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% снаружи!


использование карты:

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

мне нравится идея @ллл л!--3--> ответ, но у него есть изъян.

рассмотрим следующий тест (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..еще..другой логики, он должен быть в состоянии короткого замыкания.

здесь быстрое исправление: измените интерфейс Execute метод, чтобы возвратить bool чтобы указать, следует ли управлять огнем и добавить Value свойство, чтобы получить правило decimal значение. Кроме того, добавьте правило defulat, которое 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; }
    }
}