Я часто сталкиваюсь с методами / функциями, которые имеют дополнительный логический параметр, который управляет тем, будет ли выброшено исключение в случае сбоя или будет возвращено значение null.
Уже ведутся дискуссии о том, какой из них является лучшим выбором в этом случае, поэтому давайте не будем здесь останавливаться на этом. Смотрите, например, Возвращение магического значения, выбросить исключение или вернуть ложь при ошибке?
Давайте вместо этого предположим, что есть веская причина, почему мы хотим поддерживать оба пути.
Лично я считаю, что такой метод лучше разделить на две части: одна выдает исключение при сбое, а другая возвращает ноль при ошибке.
Итак, что лучше?
A: Один метод с $exception_on_failure
параметром.
/**
* @param int $id
* @param bool $exception_on_failure
*
* @return Item|null
* The item, or null if not found and $exception_on_failure is false.
* @throws NoSuchItemException
* Thrown if item not found, and $exception_on_failure is true.
*/
function loadItem(int $id, bool $exception_on_failure): ?Item;
Б: Два разных метода.
/**
* @param int $id
*
* @return Item|null
* The item, or null if not found.
*/
function loadItemOrNull(int $id): ?Item;
/**
* @param int $id
*
* @return Item
* The item, if found (exception otherwise).
*
* @throws NoSuchItemException
* Thrown if item not found.
*/
function loadItem(int $id): Item;
РЕДАКТИРОВАТЬ: C: Что-то еще?
Многие люди предложили другие варианты или утверждают, что и А, и В имеют недостатки. Такие предложения или мнения приветствуются, актуальны и полезны. Полный ответ может содержать такую дополнительную информацию, но также затронет основной вопрос о том, является ли параметр для изменения сигнатуры / поведения хорошей идеей.
Заметки
На случай, если кому-то интересно: примеры на PHP. Но я думаю, что вопрос применим ко всем языкам, если они чем-то похожи на PHP или Java.
источник
loadItemOrNull(id)
, подумайте,loadItemOr(id, defaultItem)
имеет ли смысл для вас. Если элемент является строкой или числом, это часто происходит.Ответы:
Вы правы: два метода намного лучше , что, по нескольким причинам:
В Java сигнатура метода, который потенциально генерирует исключение, будет включать это исключение; другой метод не будет. Это делает особенно ясным, что ожидать от одного и другого.
В таких языках, как C #, где сигнатура метода ничего не говорит об исключениях, публичные методы по-прежнему должны документироваться, и такая документация включает исключения. Документирование одного метода не будет легким.
Ваш пример идеален: комментарии во втором фрагменте кода выглядят намного яснее, и я бы даже коротко сказал «Элемент, если найден (исключение в противном случае)». Вплоть до «Элемент.» - наличие потенциального исключения и описание, которое вы дали ему, говорит само за себя.
В случае одного метода есть несколько случаев, когда вы хотите переключить значение логического параметра во время выполнения , и если вы это сделаете, это будет означать, что вызывающая сторона должна будет обрабатывать оба случая (
null
ответ и исключение) ), делая код гораздо сложнее, чем нужно. Поскольку выбор сделан не во время выполнения, а при написании кода, два метода имеют смысл.Некоторые платформы, такие как .NET Framework, создали соглашения для этой ситуации, и они решают ее двумя способами, как вы и предлагали. Единственное отличие состоит в том, что они используют шаблон, объясненный Юаном в его ответе , поэтому
int.Parse(string): int
выдает исключение, аint.TryParse(string, out int): bool
не делает. Это соглашение об именах является очень сильным в сообществе .NET и должно соблюдаться всякий раз, когда код соответствует ситуации, которую вы описываете.источник
int.Parse()
считается действительно плохим дизайнерским решением, именно поэтому команда .NET пошла дальше и внедрилаTryParse
.->itemExists($id)
перед звонком->loadItem($id)
. Или, возможно, это просто выбор, сделанный другими людьми в прошлом, и мы должны принять это как должное.data Maybe a = Just a | Nothing
).Интересным вариантом является язык Swift. В Swift вы объявляете функцию как «throwing», то есть ей разрешено выбрасывать ошибки (похожие, но не совсем такие, как, скажем, исключения Java). Вызывающая сторона может вызывать эту функцию четырьмя способами:
а. В операторе try / catch перехват и обработка исключения.
б. Если вызывающий объект объявлен как выбрасывающий, просто вызовите его, и любая выброшенная ошибка превращается в выдаваемую вызывающим.
с. Обозначение вызова функции
try!
означает «Я уверен, что этот вызов не будет сброшен». Если вызываемая функция действительно выбрасывает, приложение гарантированно завершится сбоем.д. Обозначение вызова функции
try?
означает «Я знаю, что функция может выдать, но мне все равно, какая именно ошибка выдается». Это изменяет возвращаемое значение функции с типа "T
" на тип "optional<T>
", и выброшенное исключение превращается в возвращающий ноль.(a) и (d) - случаи, о которых вы спрашиваете. Что если функция может вернуть nil и может выдавать исключения? В этом случае тип возвращаемого значения будет "
optional<R>
" для некоторого типа R, и (d) изменит это на "optional<optional<R>>
", что немного загадочно и трудно понять, но вполне нормально. И функция Swift может возвращатьoptional<Void>
, так что (d) может использоваться для броска функций, возвращающих Void.источник
Мне нравится
bool TryMethodName(out returnValue)
подход.Это дает вам окончательное указание на то, успешно ли выполнен метод, и совпадает ли наименование, заключая метод выброса исключения в блок try catch.
Если вы просто возвращаете ноль, вы не знаете, если это не удалось или возвращаемое значение было законно нулевым.
например:
пример использования (вне означает, что передача по ссылке не инициализирована)
источник
bool TryMethodName(out returnValue)
выглядеть ваш « подход» в исходном примере?Обычно логический параметр указывает, что функцию можно разделить на две части, и, как правило, вы всегда должны разбивать ее, а не передавать логическую.
источник
b
: вместо вызоваfoo(…, b);
вы должны писатьif (b) then foo_x(…) else foo_y(…);
и повторять другие аргументы или что-то вроде того,((b) ? foo_x : foo_y)(…)
если в языке есть троичный оператор и функции / методы первого класса. И это может быть даже повторено из нескольких разных мест в программе.if (myGrandmaMadeCookies) eatThem() else makeFood()
который да, я мог бы обернуть в другую функцию, как этоcheckIfShouldCook(myGrandmaMadeCookies)
. Интересно, связан ли упомянутый вами нюанс с тем, передаем ли мы логическое значение того, как мы хотим, чтобы функция работала, как в первоначальном вопросе, по сравнению с тем, как мы передаем некоторую информацию о нашей модели, как в Пример бабушки, который я только что привел.Чистый код советует избегать аргументов логических флагов, потому что их значение непрозрачно на сайте вызова:
тогда как отдельные методы могут использовать выразительные имена:
источник
Если бы мне пришлось использовать A или B, я бы использовал B, два отдельных метода, по причинам, указанным в ответе Арсения Мурзенкоса .
Но есть еще один метод 1. В Java он вызывается
Optional
, но вы можете применить ту же концепцию к другим языкам, где вы можете определять свои собственные типы, если язык еще не предоставляет подобный класс.Вы определяете свой метод так:
В вашем методе вы либо возвращаете,
Optional.of(item)
если нашли предмет, либо возвращаете, если не нашлиOptional.empty()
. Вы никогда не бросаете 2 и никогда не возвращаетесьnull
.Для клиента вашего метода это что-то вроде
null
, но с большой разницей, что заставляет задуматься о случае пропущенных предметов. Пользователь не может просто игнорировать тот факт, что не может быть никакого результата. Чтобы получить элемент изOptional
явного действия, требуется:loadItem(1).get()
скинетNoSuchElementException
или вернет найденный предмет.loadItem(1).orElseThrow(() -> new MyException("No item 1"))
возможность использовать пользовательское исключение, если возвращаемое значениеOptional
пустое.loadItem(1).orElse(defaultItem)
возвращает либо найденный элемент, либо пропущенныйdefaultItem
(что также может бытьnull
) без исключения.loadItem(1).map(Item::getName)
будет снова возвращатьOptional
с именем элементов, если присутствует.Optional
.Преимущество состоит в том, что теперь все зависит от клиентского кода, что должно произойти, если элемента нет, а вам просто нужно предоставить один метод .
1 Я думаю, что это что-то вроде ответа
try?
in gnasher729s, но он мне не совсем понятен, так как я не знаю Swift, и кажется, что это языковая функция, поэтому она специфична для Swift.2 Вы можете генерировать исключения по другим причинам, например, если вы не знаете, есть ли элемент или нет, потому что у вас были проблемы с связью с базой данных.
источник
Как еще один момент, согласившись с использованием двух методов, это может быть очень легко реализовать. Я напишу свой пример на C #, так как я не знаю синтаксис PHP.
источник
Я предпочитаю два метода, таким образом, вы не только скажете мне, что вы возвращаете значение, но и какое именно значение.
TryDoSomething () тоже неплохой шаблон.
Я больше привык видеть первое в взаимодействиях с базой данных, а второе больше используется для разбора.
Как уже говорили другие, параметры флагов обычно являются запахом кода (запахи кода не означают, что вы делаете что-то не так, просто есть вероятность, что вы делаете это неправильно). Хотя вы называете его точно, иногда есть нюансы, которые затрудняют понимание того, как использовать этот флаг, и вы в конечном итоге заглядываете внутрь тела метода.
источник
В то время как другие люди предлагают иначе, я бы предложил, чтобы метод с параметром, указывающим, как должны обрабатываться ошибки, лучше, чем требование использования отдельных методов для двух сценариев использования. Хотя может быть желательным также иметь разные точки входа для использования «выбросы ошибок» и «ошибка возвращает указание ошибки», наличие точки входа, которая может обслуживать оба шаблона использования, позволит избежать дублирования кода в методах-оболочках. В качестве простого примера, если есть функции:
и нужно, чтобы функции работали аналогично, но с х, заключенным в скобки, необходимо написать два набора функций-оболочек:
Если есть аргумент о том, как обрабатывать ошибки, потребуется только одна оболочка:
Если оболочка достаточно проста, дублирование кода может быть не таким уж и плохим, но если его когда-либо потребуется изменить, дублирование кода, в свою очередь, потребует дублирования любых изменений. Даже если вы решите добавить точки входа, которые не используют дополнительный аргумент:
Можно хранить всю «бизнес-логику» в одном методе, который принимает аргумент, указывающий, что делать в случае сбоя.
источник
Я думаю, что все ответы, которые объясняют, что у вас не должно быть флага, который контролирует, выбрасывается исключение или нет, являются хорошими. Их совет будет моим советом любому, кто чувствует необходимость задать этот вопрос.
Однако я хотел отметить, что есть существенное исключение из этого правила. Как только вы достаточно продвинулись, чтобы начать разработку API-интерфейсов, которые будут использовать сотни других людей, в некоторых случаях вы можете захотеть предоставить такой флаг. Когда вы пишете API, вы не просто пишете пуристам. Вы пишете реальным клиентам, у которых есть реальные желания. Часть вашей работы состоит в том, чтобы сделать их довольными вашим API. Это становится социальной проблемой, а не проблемой программирования, и иногда социальные проблемы диктуют решения, которые были бы далеко не идеальными в качестве самостоятельных программных решений.
Вы можете обнаружить, что у вас есть два разных пользователя вашего API, один из которых хочет исключений, а другой нет. В реальной жизни это может произойти с библиотекой, которая используется как в разработке, так и во встроенной системе. В средах разработки пользователи, вероятно, захотят иметь исключения везде. Исключения очень популярны для обработки неожиданных ситуаций. Тем не менее, они запрещены во многих встроенных ситуациях, потому что они слишком сложны для анализа ограничений в реальном времени. Когда вы на самом деле заботитесь не только о среднем времени, которое требуется для выполнения вашей функции, но и о времени, которое требуется для какого-то отдельного развлечения, идея разматывать стек в любом произвольном месте очень нежелательна.
Пример из реальной жизни для меня: математическая библиотека. Если в вашей математической библиотеке есть
Vector
класс, который поддерживает получение единичного вектора в одном и том же направлении, вы должны иметь возможность делить на величину. Если величина равна 0, у вас есть ситуация деления на ноль. В разработке вы хотите поймать это . Вы действительно не хотите удивительного деления на 0, торчащие вокруг. Бросок исключения - очень популярное решение для этого. Это почти никогда не происходит (это действительно исключительное поведение), но когда это происходит, вы хотите знать это.На встроенной платформе вы не хотите этих исключений. Было бы больше смысла сделать проверку
if (this->mag() == 0) return Vector(0, 0, 0);
. На самом деле, я вижу это в реальном коде.Теперь подумайте с точки зрения бизнеса. Вы можете попытаться научить двум различным способам использования API:
Это удовлетворяет мнениям большинства ответов здесь, но с точки зрения компании это нежелательно. Разработчики встраиваемых систем должны изучить один стиль кодирования, а разработчики - другой. Это может быть довольно неприятно, и заставляет людей думать одним образом. Потенциально хуже: как вы можете доказать, что встроенная версия не содержит кода, генерирующего исключения? Броская версия может легко вписаться, прячась где-то в вашем коде, пока дорогая комиссия по проверке ошибок не найдет ее.
Если, с другой стороны, у вас есть флаг, который включает или отключает обработку исключений, обе группы могут изучить один и тот же стиль кодирования. Фактически, во многих случаях вы можете даже использовать код одной группы в проектах другой группы. В случае использования этого кода
unit()
, если вы действительно заботитесь о том, что происходит в случае деления на 0, вы должны были написать тест самостоятельно. Таким образом, если вы перемещаете его во встроенную систему, где вы просто получаете плохие результаты, такое поведение является «нормальным». Теперь, с точки зрения бизнеса, я обучаю своих программистов один раз, и они используют одни и те же API и одинаковые стили во всех частях моего бизнеса.В подавляющем большинстве случаев вы хотите следовать советам других: используйте идиомы, подобные
tryGetUnit()
или похожие, для функций, которые не генерируют исключения, и вместо этого возвращайте значение часового типа, например, null. Если вы думаете, что пользователи могут захотеть использовать как обработку исключений, так и код обработки не исключений бок о бок в одной программе, придерживайтесьtryGetUnit()
обозначений. Тем не менее, есть угловой случай, когда реальность бизнеса может сделать использование флага лучше. Если вы окажетесь в этом угловом деле, не бойтесь отбросить «правила» на второй план. Вот для чего нужны правила!источник