Це звичайна ситуація, і існує багато загальних способів вирішити цю проблему. Ось моя спроба канонічної відповіді. Будь ласка, коментуйте, якщо я щось пропустив, і я буду постійно оновлювати цю публікацію.
Це Стріла
Те, що ви обговорюєте, відоме як анти-шаблон стрілки . Він називається стрілкою, оскільки ланцюжок вкладених ifs утворює кодові блоки, які розширюються далі та далі вправо, а потім назад вліво, утворюючи візуальну стрілку, яка "вказує" на праву частину області редактора коду.
Згладьте стрілку охоронцем
Деякі поширені способи уникнути Стріли обговорюються тут . Найпоширеніший метод полягає у використанні захисної схеми, в якій код обробляє спочатку потоки виключень, а потім обробляє основний потік, наприклад замість
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
Коли є довга серія охоронців, це значно сплющує код, оскільки всі охоронці з’являються повністю вліво, і ваш ifs не вкладений. Крім того, ви візуально поєднуєте логічну умову з пов’язаною з нею помилкою, що дозволяє значно простіше розповісти, що відбувається:
Стрілка:
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;
}
Охорона:
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 ++, ви не можете цього зробити. Але це відповідь номер один для мов, які містять нарешті ключове слово, оскільки саме це і є.
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) та перерву, як показано в інших відповідях.
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, права частина ніколи не оцінюється.
Цей трюк є найбільш корисним, коли потрібен компактний код і коли він, швидше за все, не потребує значного обслуговування, наприклад, ви реалізуєте відомий алгоритм. Для більш загального кодування структура цього коду є надто крихкою; навіть незначна зміна логіки може викликати повне перезапис.