Это обычная ситуация, и есть много способов справиться с ней. Вот моя попытка канонического ответа. Пожалуйста, прокомментируйте, если я что-то пропустил, и я буду обновлять этот пост.
Это стрелка
То, что вы обсуждаете, называется анти-паттерном стрелки . Это называется стрелкой, потому что цепочка вложенных if формирует блоки кода, которые расширяются все дальше и дальше вправо, а затем назад влево, образуя визуальную стрелку, которая «указывает» на правую сторону панели редактора кода.
Расправь стрелу с гвардией
Некоторые общие способы избежать Стрелы обсуждаются здесь . Наиболее распространенным методом является использование защитного шаблона, в котором код сначала обрабатывает потоки исключений, а затем обрабатывает основной поток, например, вместо
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... вы бы использовали ....
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Когда есть длинная серия охранников, это значительно сглаживает код, поскольку все охранники появляются полностью налево, и ваши if не являются вложенными. Кроме того, вы визуально связываете логическое условие с соответствующей ошибкой, что значительно упрощает понимание происходящего:
Стрелка:
ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops"); //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}
Guard:
ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}
Это объективно и количественно легче читать, потому что
- Символы {и} для данного логического блока расположены ближе друг к другу
- Количество ментального контекста, необходимого для понимания конкретной строки, меньше
- Вся логика, связанная с условием if, скорее всего, находится на одной странице
- Потребность в кодировщике для прокрутки трека страницы / глаза значительно уменьшена
Как добавить общий код в конце
Проблема с паттерном охраны заключается в том, что он опирается на то, что называется «оппортунистическим возвращением» или «оппортунистическим выходом». Другими словами, это нарушает схему, согласно которой каждая функция должна иметь ровно одну точку выхода. Это проблема по двум причинам:
- Некоторым людям это не по плечу, например, люди, которые научились кодировать на Паскале, узнали, что одна функция = одна точка выхода.
- Он не предоставляет раздел кода, который выполняется при выходе, независимо от того , что является предметом под рукой.
Ниже я предоставил некоторые варианты для обхода этого ограничения, либо используя языковые функции, либо избегая проблемы в целом.
Вариант 1. Вы не можете сделать это: использовать finally
К сожалению, как разработчик C ++, вы не можете сделать это. Но это ответ номер один для языков, которые содержат ключевое слово finally, поскольку именно для этого оно и предназначено.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
Вариант 2. Избегайте проблемы: реструктурируйте свои функции
Вы можете избежать этой проблемы, разбив код на две функции. Преимущество этого решения заключается в работе на любом языке, и, кроме того, оно может снизить цикломатическую сложность , которая является проверенным способом снижения уровня дефектов, и повышает специфичность любых автоматических модульных тестов.
Вот пример:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Вариант 3. Языковой трюк: использовать фальшивый цикл
Другой распространенный трюк, который я вижу, - это использование while (true) и break, как показано в других ответах.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Хотя это менее "честно", чем использование goto
, оно менее подвержено путанице при рефакторинге, так как оно четко отмечает границы логической области видимости. Наивный программист, который вырезает и вставляет ваши ярлыки или ваши goto
заявления, может вызвать серьезные проблемы! (И, честно говоря, картина настолько распространена, что теперь я думаю, что она ясно сообщает о намерениях и, следовательно, вовсе не является «нечестной»).
Есть и другие варианты этой опции. Например, можно использовать switch
вместо while
. Любая языковая конструкция с break
ключевым словом, вероятно, будет работать.
Вариант 4. Использование жизненного цикла объекта
Еще один подход использует жизненный цикл объекта. Используйте объект контекста для переноса ваших параметров (то, чего подозрительно не хватает нашему наивному примеру) и избавьтесь от него, когда закончите.
class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}
//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}
Примечание. Убедитесь, что вы понимаете жизненный цикл объекта по своему выбору. Для этого вам нужен какой-то детерминированный сборщик мусора, т.е. вы должны знать, когда будет вызван деструктор. В некоторых языках вам нужно будет использовать Dispose
вместо деструктора.
Вариант 4.1. Использовать жизненный цикл объекта (шаблон оболочки)
Если вы собираетесь использовать объектно-ориентированный подход, можете сделать это правильно. Эта опция использует класс, чтобы «обернуть» ресурсы, которые требуют очистки, а также другие его операции.
class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}
void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed
Опять же, убедитесь, что вы понимаете жизненный цикл вашего объекта.
Вариант 5. Языковой трюк: использовать оценку короткого замыкания
Другой метод заключается в использовании оценки короткого замыкания .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Это решение использует преимущества работы оператора &&. Когда левая часть && имеет значение false, правая часть никогда не оценивается.
Этот трюк наиболее полезен, когда требуется компактный код, и когда код вряд ли нуждается в большом обслуживании, например, вы реализуете хорошо известный алгоритм. Для более общего кодирования структура этого кода слишком хрупкая; даже незначительное изменение в логике может вызвать полное переписывание.