Должен ли конструктор Perl возвращать undef или "недопустимый" объект?

Вопрос

Что считается "Лучшей практикой" - и почему - обработки ошибок в конструкторе?.

"Лучшая практика" может быть цитатой от Schwartz, или 50% модулей CPAN используют ее и т.д.; но я доволен хорошо аргументированным мнением любого, даже если он объясняет, почему общая наилучшая практика - это не лучший подход.

Что касается моего собственного мнения по этой теме (в течение многих лет я рассказывал о разработке программного обеспечения на Perl), я видел три основных подхода к обработке ошибок в модуле perl (по моему мнению, от лучших до худших):

  • Построить объект, установить недопустимый флаг (обычно "is_valid" ). Часто в сочетании с установкой сообщения об ошибке с помощью обработки ошибок класса.

    Плюсы:

    • Позволяет выполнять стандартные (по сравнению с другими вызовами) обработку ошибок, поскольку позволяет использовать вызовы типа $obj->errors() после неудачного конструктора, как и после любого другого вызова метода.

    • Позволяет передавать дополнительную информацию (например, > 1 ошибка, предупреждения и т.д.)

    • Позволяет использовать легкую функциональность "redo" / "fixme". Другими словами, если объект, который был сконструирован, очень тяжелый, со многими сложными атрибутами, которые на 100% всегда в порядке, и единственная причина, по которой это не действительный, потому что кто-то ввел неверную дату, вы можете просто сделать "$obj->setDate()" вместо накладных расходов на повторное выполнение всего конструктора. Этот шаблон не всегда необходим, но может быть чрезвычайно полезным в правильном дизайне.

    Минусы: Нет, о которых я знаю.

  • Вернуть "undef".

    Минусы: невозможно достичь ни одного из преимуществ первого решения (сообщения об ошибках для объектов за пределами глобальных переменных и облегченная "фиксированная" возможность для тяжелых объектов).

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

  • ОБНОВЛЕНИЕ. Чтобы быть ясным, я считаю, что решение (в противном случае очень достойное и отличное решение) имеет очень простой конструктор, который не может вообще терпеть неудачу, и тяжелый метод инициализации, где вся проверка ошибок происходит быть просто подмножеством в любом случае # 1 (если инициализатор устанавливает флаги ошибок) или случай №3 (если инициализатор умирает) для целей этого вопроса. Очевидно, выбирая такую ​​конструкцию, вы автоматически отклоняете опцию № 2.

Ответ 1

Это зависит от того, как вы хотите, чтобы ваши конструкторы вели себя.

Остальная часть этого ответа попадает в мои личные наблюдения, но, как и большинство вещей Perl, Best Practices действительно сводится к "Вот один из способов сделать это, который вы можете взять или оставить в зависимости от ваших потребностей". Ваши предпочтения, как вы их описали, полностью действительны и последовательны, и никто не должен сообщать вам об этом.

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

С другой стороны, если вы предпочитаете, чтобы этого не произошло, я думаю, что предпочел бы 2 более 1, потому что так же легко проверить объект undefined, как и для проверки некоторой переменной флага. Это не C, поэтому у нас нет сильного ограничения на печать, говорящего нам, что наш конструктор ДОЛЖЕН вернуть объект этого типа. Поэтому возвращение undef и проверка на это для успеха или неудачи - отличный выбор.

"Накладные расходы" сбоя в строительстве рассматриваются в некоторых случаях с краями (где вы не можете быстро выйти из строя до накладных расходов), поэтому для тех, кого вы можете предпочесть метод 1. Таким образом, это зависит от того, какую семантику вы используете для построения объекта. Например, я предпочитаю делать супертяжелую инициализацию вне конструкции. Что касается стандартизации, я считаю, что проверка того, возвращает ли конструктор определенный объект, является хорошим стандартом, как проверка переменной флага.

EDIT: В ответ на ваше изменение об инициализаторах, отклоняющих случай №2, я не понимаю, почему инициализатор не может просто вернуть значение, указывающее на успех или неудачу, а не на установку переменной флага. На самом деле, вы можете использовать оба варианта, в зависимости от того, сколько деталей вы хотите узнать о произошедшей ошибке. Но для инициализатора было бы совершенно справедливо возвращать true при успешном завершении и undef при сбое.

Ответ 2

Я предпочитаю:

  • Сделайте как можно меньше инициализации в конструкторе.
  • croak с информативным сообщением, когда что-то пойдет не так.
  • Используйте соответствующие методы инициализации для предоставления сообщений об ошибках объектов и т.д.

Кроме того, возврат undef (вместо каркаса) прекрасен, если пользователям класса может быть не все равно, почему именно произошел сбой, только если они получили действительный объект или нет.

Я презираю легко забыть методы is_valid или добавлять дополнительные проверки, чтобы гарантировать, что методы не вызывают, когда внутреннее состояние объекта не определено.

Я говорю это с очень субъективной точки зрения, не делая никаких заявлений о лучших практиках.

Ответ 3

Я бы порекомендовал против # 1 просто потому, что это приводит к большему количеству кода обработки ошибок, который не будет записан. Например, если вы просто вернете false, это будет хорошо.

my $obj = Class->new or die "Construction failed...";

Но если вы вернете недопустимый объект...

my $obj = Class->new;
die "Construction failed @{[ $obj->error_message ]}" if $obj->is_valid;

И по мере увеличения количества кода обработки ошибок вероятность его записи уменьшается. И это не линейно. Увеличивая сложность вашей системы обработки ошибок, вы фактически уменьшаете количество ошибок, которые она поймает в практическом использовании.

Также необходимо быть осторожным, чтобы ваш недопустимый объект умирал при вызове любого метода (кроме is_valid и error_message), что приводило к еще большему количеству кода и возможности для ошибок.

Но я согласен, что есть возможность получить информацию об ошибке, что делает возвращение false (только return not return undef) ниже. Традиционно это делается путем вызова метода класса или глобальной переменной, как в DBI.

my $dbh = DBI- > connect ($ data_source, $username, $password)             или умереть $DBI:: errstr;

Но он страдает от A) вам все равно придется писать код обработки ошибок, а B) его действительный только для последней операции.

Лучше всего сделать, в общем, бросить исключение с croak. Теперь в нормальном случае пользователь не пишет специальный код, ошибка возникает в точке проблемы, и по умолчанию они получают хорошее сообщение об ошибке.

my $obj = Class->new;

Традиционные рекомендации Perl относительно исключения исключений в библиотечном коде как невежливые устарели. Программисты Perl (наконец) охватывают исключения. Вместо того, чтобы писать код обработки ошибок снова и снова, плохо и часто забывая, исключения DWIM. Если вы не уверены, просто начните использовать autodie (смотреть видео pjf об этом), и вы никогда не вернетесь.

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

my $obj = eval { Class->new } or do { something else };

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

eval {
    run_the_main_web_code();
} or do {
    log_the_error([email protected]);
    print_the_pretty_error_page;
};

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

my $users = eval { Users->search({ name => $name }) } or do {
    ...handle an error while finding a user...
};

Там происходит две вещи. 1) Users->search всегда возвращает истинное значение, в этом случае массив ref. Это упрощает работу my $obj = eval { Class->method } or do. Это необязательно. Но, что более важно, 2) вам нужно только установить специальную обработку ошибок вокруг Users->search. Все методы, называемые внутри Users->search, и все методы, которые они называют... они просто бросают исключения. И они все пойманы в один момент и обрабатывают то же самое. Обработка исключения в той точке, которая его интересует, делает гораздо более аккуратный, компактный и гибкий код обработки ошибок.

Вы можете упаковать больше информации в исключение с помощью croak ing с перегруженным строкой объекта, а не только с строкой.

my $obj = eval { Class->new }
  or die "Construction failed: [email protected] and there were @{[ [email protected]>num_frobnitz ]} frobnitzes";

Исключения:

  • Выполняйте правильные действия без каких-либо соображений со стороны вызывающего абонента
  • Требовать наименьший код для наиболее распространенного случая
  • Обеспечьте максимальную гибкость и информацию об отказе вызывающего абонента

Модули, такие как Try:: Tiny, исправляют большинство проблем, связанных с использованием eval в качестве обработчика исключений.

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

Ответ 4

Сначала помпезные общие наблюдения:

  • Задача конструктора должна быть: Если заданы допустимые параметры построения, верните действительный объект.
  • Конструктор, который не создает действительный объект, не может выполнять свою работу и поэтому является идеальным кандидатом на создание исключений.
  • Убедиться, что построенный объект действителен, является частью задания конструктора. Выдавать объект, который может быть плохой, и полагаться на клиента, чтобы проверить, что объект действителен, является верным способом завершить работу с недействительными объектами, которые взрываются в отдаленных местах по неочевидным причинам.
  • Проверка наличия правильных аргументов до того, как вызов конструктора является заданием клиента.
  • Исключения обеспечивают мелкомасштабный способ распространения конкретной ошибки, которая произошла без необходимости иметь сломанный объект в руке.
  • return undef; всегда плохо [1]
  • bIlujDI 'yIchegh() Qo'; yIHegh (!)

Теперь к фактическому вопросу, который я буду толковать, означает "что вы, darch, считаете лучшей практикой и почему". Во-первых, я заметил, что возврат ложного значения при неудаче имеет длинную историю Perl (например, основная часть ядра работает, например), и многие модули следуют этому соглашению. Тем не менее, оказывается, что это соглашение создает код нижнего клиента, а более новые модули от него отходят. [2]

[Вспомогательный аргумент и примеры кода для этого оказываются более общим случаем для исключений, которые вызвали создание autodie, и поэтому я буду сопротивляться искушению сделать этот случай здесь. Вместо этого:]

Необходимость проверки успешного создания на самом деле более обременительна, чем проверка исключения на соответствующем уровне обработки исключений. Другие решения требуют, чтобы непосредственный клиент выполнял больше работы, чем должен был просто получить объект, работу, которая не требуется, когда конструктор терпит неудачу, выбрасывая исключение. [3] Исключением является гораздо более выразительным, чем undef и в равной степени выразительным, как передача назад сломанного объекта для документирования ошибок и аннотирование их на разных уровнях в стеке вызовов.

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

Итак: конструктор, который не может создать допустимый объект, должен как можно раньше исключить исключение. Исключения, которые может выполнить конструктор, должны быть документированными частями его интерфейса. Только вызывающие уровни, которые могут осмысленно действовать на исключение, должны даже искать его; очень часто поведение "если эта конструкция терпит неудачу, ничего не делает", является абсолютно правильной.

[1]: Под этим я подразумеваю, что я не знаю о каких-либо случаях использования, когда return; не является строго превосходным. Если кто-то назовет меня этим, мне, возможно, придется открыть вопрос. Поэтому, пожалуйста, не надо.;)
[2]: за мое крайне ненаучное воспоминание о интерфейсах модулей, которые я читал за последние два года, с учетом смещения выбора и подтверждения.
[3]: Обратите внимание, что для исключения исключения по-прежнему требуется обработка ошибок, как и другие предлагаемые решения. Это не означает обертывание каждого экземпляра в eval, если вы на самом деле не хотите выполнять сложную обработку ошибок вокруг каждой конструкции (и если вы думаете, что это так, вы, вероятно, ошибаетесь). Это означает завершение вызова, который способен осмысленно действовать на исключение в eval.