Как написать часть метода проверки ошибок, чтобы сделать его читаемым и подверженным ошибкам?

У меня было разногласие с другим программистом о том, как написать метод с большой проверкой ошибок:

public void performAction() {
  if (test1) {
    if (test2) {
      if (test3) {
        // DO STUFF
      } else {
        return "error 3";
      }
    } else {
      return "error 2";
    }
  } else {
    return "error 1";
  }
}

-

public void performAction() {     
  if (!test1) {
    return "error 1";
  }
  if (!test2) {
    return "error 1";
  }
  if (!test3) {
    return "error 1";
  }
  // DO STUFF
}

Для меня глубокое вложение операторов if затрудняет чтение первого примера.
Второй, несмотря на наличие трех возвращаемых s, более читабелен.

Я с любопытством проверил, что Code Complete говорит по этому поводу, и у меня не осталось уверенности в том, как с этим справиться

Стек состояний ошибок в нижней части гнезда является признаком хорошо написанного кода обработки ошибок.

но потом

Отступ в основной части подпрограммы внутри четырех операторов if выглядит эстетично, особенно если внутри самого оператора if много кода.

и рассматривая использование пунктов охраны, как во втором примере

Минимизируйте количество возвратов в каждой процедуре. Труднее понять рутину, когда, читая ее внизу, вы не подозреваете, что она вернулась где-то выше.

Как написать часть метода проверки ошибок, чтобы сделать его читаемым и подверженным ошибкам?

3 ответа

Решение

Ничто не заставляет программистов вступать в борьбу быстрее, чем стилистические дебаты ( выгодно ли когда-либо использовать "goto" в языке, который поддерживает циклы и функции? Если так, то почему?). Таким образом, короткий ответ: "Какой стиль вы и ваша команда решите, лучше всего подходит для вашего проекта / языка".

При этом я хотел бы добавить свои 2 цента в комментарий Code Complete о множественных возвратах. Вы должны различать несколько успешных возвратов и несколько возвратов. Если мне нужно исследовать 5 возвратов, которые не связаны с генерируемыми ошибками, возможно, необходимо переписать функцию. Если вы корректно выйдете из своей функции сразу после обнаружения ошибки, то у программиста по техническому обслуживанию (т. Е. У вас через 6 месяцев) не должно быть больше проблем с выполнением основной логики вашей функции, чем если бы вы вложили все эти проверки ошибок.

Итак, мое личное мнение таково, что ваш второй пример кода - лучший выбор.

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

Идея попытаться сместить функции выхода к нижней части области была полезна в дни, предшествовавшие обработке исключений и автоматическому управлению ресурсами (например, языки без деструкторов или GC, такие как C), потому что для устранения ошибок часто требовалась ручная очистка.

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

В таких случаях, как при сборке, довольно часто можно увидеть jumps/branches до метки ошибки в нижней части функции, где произойдет очистка. Это также не слишком редко, даже в C, используя gotos для этого.

Кроме того, как уже упоминалось, глубокое вложение вводит много психических накладных расходов. Ваш мозг должен функционировать как глубоко вложенный стек, пытаясь вспомнить, где вы находитесь, и как даже Линус Торвальдс, несгибаемый C-кодер, любит говорить: если вам нужно что-то вроде 4-х вложенных уровней отступов в одной функции, ваш код уже сломан и должен быть реорганизован (я не уверен, что согласен с ним насчет точного числа, но у него есть точка зрения с точки зрения того, как это запутывает логику).

Когда вы переходите на более современный язык, такой как C++, у вас теперь есть автоматизированное управление ресурсами с помощью деструкторов. В этом случае функции больше не должны упоминать подробности очистки, так как ресурсы должны обрабатывать очистку автоматически, в соответствии с тем, что называется сбором ресурсов - это идиома инициализации (не совсем лучшее имя). Это устраняет одну из главных причин отдавать предпочтение стилю, который стремится к логике обработки ошибок.

Кроме того, когда вы используете язык, такой как C++, он потенциально генерирует исключения и повсеместно. Поэтому для каждой другой строки кода нередко возникает эффект скрытого, неявного выхода с такой логикой:

if an exception occurs:
    automatically cleanup resources and propagate the error

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

if something bad happened:
     return error

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

if something exceptional happened:
    return error

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

if something normal happened:
     ...
     return success
return error

Это мое мнение.

Старая мантра "Минимизировать количество возвратов в каждой программе", похоже, немного устарела. Это очень удобно, когда у вас есть методы длиннее 8-10 строк кода, где выполняется много операций.

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

В любом случае, каждый раз, когда вы вкладываете ifs, читаемость существенно страдает.

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

Образец кода:

public void Execute() 
{     
    if (test1)
    {
        return;
    }
    else if (test2)
    {
        return;
    }
    PerformAction();
}

private void PerformAction()
{
    //DO STUFF
}
Другие вопросы по тегам