Как написать часть метода проверки ошибок, чтобы сделать его читаемым и подверженным ошибкам?
У меня было разногласие с другим программистом о том, как написать метод с большой проверкой ошибок:
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
}