Как вы справляетесь с огромными if-условиями?

Это то, что меня беспокоит на каждом языке, который я использовал, у меня есть оператор if, но в условной части так много проверок, что мне приходится разбивать его на несколько строк, использовать вложенный оператор if или просто признать, что он уродлив, и двигаться дальше с моей жизнью.

Есть ли какие-то другие методы, которые вы нашли, которые могут быть полезны мне и кому-либо еще, столкнувшемуся с той же проблемой?

Пример, все в одной строке:

if (var1 = true && var2 = true && var2 = true && var3 = true && var4 = true && var5 = true && var6 = true)
{

Пример, многострочный:

if (var1 = true && var2 = true && var2 = true
 && var3 = true && var4 = true && var5 = true
 && var6 = true)
{

Вложенный пример:

if (var1 = true && var2 = true && var2 = true && var3 = true)
{
     if (var4 = true && var5 = true && var6 = true)
     {

Ответов (21)

Решение

Разделите условие на несколько логических значений, а затем используйте главное логическое значение в качестве условия.

bool isOpaque = object.Alpha == 1.0f;
bool isDrawable = object.CanDraw && object.Layer == currentLayer;
bool isHidden = hideList.Find(object);

bool isVisible = isOpaque && isDrawable && ! isHidden;

if(isVisible)
{
    // ...
}

Еще лучше:

public bool IsVisible {
    get
    {
        bool isOpaque = object.Alpha == 1.0f;
        bool isDrawable = object.CanDraw && object.Layer == currentLayer;
        bool isHidden = hideList.Find(object);

        return isOpaque && isDrawable && ! isHidden;
    }
}

void Draw()
{
     if(IsVisible)
     {
         // ...
     }
}

Убедитесь, что вы дали своим переменным имя, которое на самом деле указывает на намерение, а не на функцию. Это очень поможет разработчику поддерживать ваш код ... это можете быть ВЫ!

Как уже упоминали другие, я бы проанализировал ваши условия, чтобы увидеть, есть ли способ передать их другим методам для повышения читабельности.

Мне нравится разбивать каждое условие на описательные переменные.

bool isVar1Valid, isVar2Valid, isVar3Valid, isVar4Valid;
isVar1Valid = ( var1 == 1 )
isVar2Valid = ( var2.Count >= 2 )
isVar3Valid = ( var3 != null )
isVar4Valid = ( var4 != null && var4.IsEmpty() == false )
if ( isVar1Valid && isVar2Valid && isVar3Valid && isVar4Valid ) {
     //do code
}
    if (   (condition_A)
        && (condition_B)
        && (condition_C)
        && (condition_D)
        && (condition_E)
        && (condition_F)
       )
    {
       ...
    }

в отличие от

    if (condition_A) {
       if (condition_B) {
          if (condition_C) {
             if (condition_D) {
                if (condition_E) {
                   if (condition_F) {
                      ...
                   }
                }
             }
          }
       }
    }

а также

    if (   (   (condition_A)
            && (condition_B)
           )
        || (   (condition_C)
            && (condition_D)
           )
        || (   (condition_E)
            && (condition_F)
           )
       )
    {
       do_this_same_thing();
    }

в отличие от

    if (condition_A && condition_B) {
       do_this_same_thing();
    }
    if (condition_C && (condition_D) {
       do_this_same_thing();
    }
    if (condition_E && condition_F) {
       do_this_same_thing();
    }

Большинство инструментов статического анализа для изучения кода будут жаловаться, если в нескольких условных выражениях не используются явные скобки, определяющие анализ выражений, вместо того, чтобы полагаться на правила приоритета операторов и меньшее количество скобок.

Вертикальное выравнивание на одном уровне отступа открытых / закрывающих фигурных скобок {}, открывающих закрывающих круглых скобок (), условных выражений с круглыми скобками и операторов слева - очень полезная практика, которая значительно УЛУЧШАЕТ читаемость и ясность кода, а не заглушает все которые могут быть втиснуты в одну строку без вертикального выравнивания, пробелов или скобок

Правила приоритета операторов сложны, например, && имеет более высокий приоритет, чем ||, но | имеет приоритет перед &&

Так, ...

    if (expr_A & expr_B || expr_C | expr_D & expr_E || expr_E && expr_F & expr_G || expr_H {
    }

- это действительно простое множественное условное выражение, которое простые люди могут неправильно прочитать и оценить.

    if (   (  (expr_A)
            & (expr_B)
           )
        || (  (expr_C)
            | (  (expr_D)
               & (expr_E)
              )
           )
        || (   (expr_E)
            && (  (expr_F)
                & (expr_G)
               )
           )
        || (expr_H)
       )
    {
    }

Нет ничего плохого в горизонтальном пространстве (перевод строки), вертикальном выравнивании или явной оценке выражений, определяющих скобки, - все это УЛУЧШАЕТ удобочитаемость и ясность.

Я часто разбиваю их на логические переменные компонентов:

bool orderValid = orderDate < DateTime.Now && orderStatus != Status.Canceled;
bool custValid = customerBalance == 0 && customerName != "Mike";
if (orderValid && custValid)
{
...

Здесь необходимо решить две проблемы: читаемость и понятность.

Решение «читабельности» - это проблема стиля, и как таковая она открыта для интерпретации. Я предпочитаю следующее:

if (var1 == true && // Explanation of the check
    var2 == true && // Explanation of the check
    var3 == true && // Explanation of the check
    var4 == true && // Explanation of the check
    var5 == true && // Explanation of the check
    var6 == true)   // Explanation of the check
    { }

или это:

if (var1 && // Explanation of the check
    var2 && // Explanation of the check
    var3 && // Explanation of the check
    var4 && // Explanation of the check
    var5 && // Explanation of the check
    var6)   // Explanation of the check
    { }

Тем не менее, такую ​​сложную проверку может быть довольно сложно мысленно проанализировать при сканировании кода (особенно если вы не являетесь первоначальным автором). Подумайте о создании вспомогательного метода, чтобы отвлечься от некоторых сложностей:

/// <Summary>
/// Tests whether all the conditions are appropriately met
/// </Summary>
private bool AreAllConditionsMet (
    bool var1,
    bool var2,
    bool var3,
    bool var4,
    bool var5,
    bool var6)
{
    return (
        var1 && // Explanation of the check
        var2 && // Explanation of the check
        var3 && // Explanation of the check
        var4 && // Explanation of the check
        var5 && // Explanation of the check
        var6);  // Explanation of the check
}

private void SomeMethod()
{
    // Do some stuff (including declare the required variables)
    if (AreAllConditionsMet (var1, var2, var3, var4, var5, var6))
    {
        // Do something
    }
}

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

Я думаю, это формально известно как шаблон рефакторинга «Разобрать условный». Такие инструменты, как Resharper или Refactor Pro! может облегчить выполнение такого рода рефакторинга!

Во всех случаях ключом к читаемому и понятному коду является использование реалистичных имен переменных. Хотя я понимаю , что это надуманный пример, «var1», «var2», и т.д., не приемлемые имена переменных. У них должно быть имя, которое отражает основную природу данных, которые они представляют.

Макдауэлл,

Вы правы, когда используете единственный оператор '&', который оценивают обе стороны выражения. Однако при использовании оператора '&&' (по крайней мере, в C#) первое выражение, которое возвращает false, является последним вычисленным выражением. Это делает вывод вычисления перед оператором FOR так же хорошо, как и любой другой способ сделать это.

Я прибегаю к отдельным логическим значениям:

Bool cond1 == (var1 && var2);
Bool cond2 == (var3 && var4);

if ( cond1 && cond2 ) {}

Ознакомьтесь с шаблонами реализации Кента Бека. Я думаю, что есть особый паттерн, который может помочь в этой ситуации ... он называется «Стражи». Вместо того, чтобы иметь множество условий, вы можете превратить их в охрану, которая проясняет, какие неблагоприятные условия в методе.

Так, например, если у вас есть метод, который что-то делает, но есть определенные условия, при которых он не должен что-то делать, а не:

public void doSomething() {
    if (condition1 && condition2 && condition3 && condition4) {
        // do something
    }
}

Вы можете изменить его на:

public void doSomething() {
    if (!condition1) {
        return;
    }

    if (!condition2) {
        return;
    }

    if (!condition3) {
        return;
    }

    if (!condition4) {
        return;
    }

    // do something
}

Он немного более подробный, но гораздо более читабельный, особенно когда у вас появляется странное вложение, сторож может помочь (в сочетании с методами извлечения).

Кстати, я НАСТОЯТЕЛЬНО рекомендую эту книгу.

Я видел много людей и редакторов, которые либо делали отступ для каждого условия в вашем операторе if одной вкладкой, либо сопоставляли его с открытым пареном:

if (var1 == true
    && var2 == true
    && var3 == true
   ) {
    /* do something.. */
}

Я обычно помещаю закрывающую скобку в ту же строку, что и последнее условие:

if (var1 == true
    && var2 == true
    && var3 == true) {
    /* do something.. */
}

Но я не думаю, что это так чисто.

Я удивлен, что этого еще никто не получил. Специально для этого типа проблем есть рефакторинг:

http://www.refactoring.com/catalog/decomposeConditional.html

@tweakt

Не лучше, но то, что я делал в прошлом:

логическое ok = cond1; ок & = cond2; ок & = cond3; ок & = cond4; ок & = cond5; ок & = cond6;

Это то же самое, что:

ok = (cond1 && cond2 && cond3 && cond4 && cond5 && cond6);

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

Для удобочитаемости я лично предпочитаю предложение Майка Стоуна, приведенное выше. Легко дать подробные комментарии и сохранить все вычислительные преимущества возможности раннего выхода. Вы также можете использовать ту же технику в функции, если это запутает организацию вашего кода, так как условное вычисление переместится подальше от другой вашей функции. Это немного банально, но вы всегда можете сделать что-нибудь вроде:

do {
    if (!cond1)
       break;
    if (!cond2)
       break;
    if (!cond3)
       break;
    ...
    DoSomething();
} while (false);

в то время как (ложь) немного глупо. Я бы хотел, чтобы у языков был оператор области видимости под названием «once» или что-то такое, от чего можно было бы легко избавиться.

Ну, во-первых, почему бы и нет:

if (var1 && var2 && var2 && var3 && var4 && var5 && var6) {
...

Кроме того, очень сложно реорганизовать абстрактные примеры кода. Если бы вы показали конкретный пример, было бы легче найти более подходящий шаблон для решения проблемы.

Не лучше, но то, что я делал в прошлом: (Следующий метод предотвращает короткое замыкание логического тестирования, все тесты запускаются, даже если первое ложно. Не рекомендуемый шаблон, если вы не знаете, что вам нужно всегда выполнять все код перед возвратом - Спасибо ptomato за обнаружение моей ошибки!)

логическое ok = cond1;
ок & = cond2;
ок & = cond3;
ок & = cond4;
ок & = cond5;
ок & = cond6;

Это то же самое, что: (не то же самое, см. Примечание выше!)

ok = (cond1 && cond2 && cond3 && cond4 && cond5 && cond6);

Если вы сделаете это:

if (var1 == true) {
    if (var2 == true) {
        if (var3 == true) {
            ...
        }
    }
}

Затем вы также можете реагировать на случаи, когда что-то не так. Например, если вы проверяете ввод, вы можете дать пользователю совет, как его правильно отформатировать, или что-то еще.

Во-первых, я бы удалил все == true части, что сделало бы его на 50% короче;)

Когда у меня большое состояние, я ищу причины. Иногда я вижу, что нужно использовать полиморфизм, иногда мне нужно добавить какой-то объект состояния. По сути, это означает, что нужен рефакторинг (запах кода).

Иногда я использую законы Де-Моргана, чтобы немного упростить логические выражения.

Мне нравится разбивать их по уровням, поэтому я бы отформатировал вам пример следующим образом:

if (var1 = true
 && var2 = true
 && var2 = true
 && var3 = true
 && var4 = true
 && var5 = true
 && var6 = true){

Это удобно, когда у вас больше вложений, как это (очевидно, что реальные условия были бы более интересными, чем "= true" для всего):

if ((var1 = true && var2 = true)
 && ((var2 = true && var3 = true)
  && (var4 = true && var5 = true))
 && (var6 = true)){

Если вам довелось программировать на Python, это удобно со встроенной all() функцией, применяемой к списку ваших переменных (здесь я просто буду использовать логические литералы):

>>> L = [True, True, True, False, True]
>>> all(L) # True, only if all elements of L are True.
False
>>> any(L) # True, if any elements of L are True.
True

Есть ли на вашем языке соответствующая функция (C#? Java?). Если да, то это, вероятно, самый чистый подход.

Попробуйте взглянуть на Функторы и Предикаты. В проекте Apache Commons есть отличный набор объектов, позволяющий инкапсулировать условную логику в объекты. Пример их использования доступен на Oreilly здесь . Отрывок из примера кода:

import org.apache.commons.collections.ClosureUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.functors.NOPClosure;

Map predicateMap = new HashMap();

predicateMap.put( isHonorRoll, addToHonorRoll );
predicateMap.put( isProblem, flagForAttention );
predicateMap.put( null, ClosureUtils.nopClosure() );

Closure processStudents = 
    ClosureUtils.switchClosure( predicateMap );

CollectionUtils.forAllDo( allStudents, processStudents );

Теперь подробности всех этих предикатов isHonorRoll и замыканий, используемых для их оценки:

import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;

// Anonymous Predicate that decides if a student 
// has made the honor roll.
Predicate isHonorRoll = new Predicate() {
  public boolean evaluate(Object object) {
    Student s = (Student) object;

    return( ( s.getGrade().equals( "A" ) ) ||
            ( s.getGrade().equals( "B" ) && 
              s.getAttendance() == PERFECT ) );
  }
};

// Anonymous Predicate that decides if a student
// has a problem.
Predicate isProblem = new Predicate() {
  public boolean evaluate(Object object) {
    Student s = (Student) object;

    return ( ( s.getGrade().equals( "D" ) || 
               s.getGrade().equals( "F" ) ) ||
             s.getStatus() == SUSPENDED );
  }
};

// Anonymous Closure that adds a student to the 
// honor roll
Closure addToHonorRoll = new Closure() {
  public void execute(Object object) {
    Student s = (Student) object;

    // Add an award to student record
    s.addAward( "honor roll", 2005 );
    Database.saveStudent( s );
  }
};

// Anonymous Closure flags a student for attention
Closure flagForAttention = new Closure() {
  public void execute(Object object) {
    Student s = (Student) object;

    // Flag student for special attention
    s.addNote( "talk to student", 2005 );
    s.addNote( "meeting with parents", 2005 );
    Database.saveStudent( s );
  }
};

Совет Стива Макконелла из Code Complete : используйте многомерную таблицу. Каждая переменная служит индексом для таблицы, а оператор if превращается в поиск по таблице. Например, если (size == 3 && weight> 70) переводится в решение о входе в таблицу [размер] [группа_веса]

Если бы я делал это на Perl, я мог бы запускать проверки именно так.

{
  last unless $var1;
  last unless $var2;
  last unless $var3;
  last unless $var4;
  last unless $var5;
  last unless $var6;

  ... # Place Code Here
}

Если вы планируете использовать это по подпрограмме заменить каждый экземпляр last с return ;

В рефлексивных языках, таких как PHP, вы можете использовать переменные-переменные:

$vars = array('var1', 'var2', ... etc.);
foreach ($vars as $v)
    if ($$v == true) {
        // do something
        break;
    }