Pull to refresh

Comments 137

7. Завязывать бизнес-логику на исключения — плохо. Например, если не получается подключиться к БД, то исключение кидать уместно, а если у пользователя недостаточно денег, чтобы купить себе какую-то цацку, то лучше обойтись без исключения.

8. Исключения делают для себя и для своих коллег, а не для пользователей. Никаких translateException, как в 5ом пункте.
7. Хорошо давайте представим себе код, который работает без исключений в данном примере. У нас 2 варианта, либо кусок кода который отвечает за проверку баланса (а так же открывает транзакцию к бд, после считывания баланса получает лок на таблицу) держать в контролере, либо сделать кастомный механизм передачи ошибок из метода в контролер. Второй вариант мне не нравится, так как у меня уже есть один метод обработки ошибок — исключения. Если же хранить весь код в контролере, то получается размазывание модели по контролеру.
8. Как быть, если мне действительно нужно передать, вот такое сложное сообщение? Вариант написать код на if else и передать любой другой объект (ассоциативный массив) с этим сообщением. Те же яйца, только в профиль.
7. но я не предлагаю работать без исключений. я предлагаю считать исключительной ситуацией падение БД, а если у юзера нет денег, то это не исключительная ситуация.
соответственно,
function getUserBalance($user) {//throws dbException

return $row['balance'];
}

class CackaController {
function buySomething() {
try {
if (getUserBalance() >= 10) {
return true;
//или return AjaxObject(array('result'=>true, 'userMessage'=>'вы купили ху*ню'));
} else {
return false;
//или return AjaxObject(array('result'=>false, 'userMessage'=>'вы нищеброд'));
}
} catch (dbException $e) {
logException($e);
sendEmailToAdmin($e);
die(«ТЕХНИЧЕСКИЕ ПРОБЛЕМЫ ПОДОЖДИТЕ 5 МИНУТ»);
}
}
Я не увидел ни строчки про лок таблицы с балансом, т.е. возможна ситуация когда два инстанса кода сначала проверят баланс (он достаточный), после оба так же удачно снимут с него деньги, когда их фактичеки не хватает. Может этот пример с одним пользователем кажется не актуальным, а если речь идет про счет в банке у которого много пользователей или кроме проверки баланса еще идет запрос (растянутый во времени) на другой сервис…
class Db{
function lockтаблицаСБалансом() {
if (!$this->execute('lock tablewithbalance)) throw new dbException();
}
}

function getUserBalance($user) {//throws dbException
$db = Db::Connect('billingdatabase');
$db->lockтаблицаСБалансом();
return $row['balance'];
}
а, я понял.

во-первых, следует разделять операции «проверка баланса» и «списание средств».
во-вторых, для того, чтобы операции были атомарными, стоит пользоваться средствами СУБД. начиная от update balancetable set balance=balance-10 where balance>=10 and userid=1 и заканчивая нормальным CALL DoCharge(1, 10);

в-третьих, если уж у нас есть функция chargeUser($user, $sum), то она должна возвращать true в случае успеха, false, если у пользователя мало денег, и кидать исключение, если у нас какая-то проблема с базой.
потому что недостаток денег на счёте не является исключительной ситуацией. это не исключительная ситуация, это стандартный вариант, он наверняка прописан в юзекейсе и в ТЗ и даже в макетах наверное нарисовано сообщение об ошибке. КАКОЕ ЭТО БЛИН ИСКЛЮЧЕНИЕ?
Мы приближаемся.
Ок, метод возвращает true/false. Т.е фактически мы создали новую схему обработки ошибок. Если все нормально возвращаем true, если все плохо возвращаем false. И того у нас два варианта один позитивный один негативный. Теперь представим, что логика немного сложнее, чем два варианта — 1 позитивный и 3 негативных. Начинаем, изобретать велосипед под названием кастомный механизм работы с ошибками. А смысл?
ну, давайте тогда уж вы и пример приведёте
Это не ошибки, а бизнес-логика. Исключение следует генерировать лишь тогда, когда дальнейшая нормальная работа приложения в конкретной ситуации невозможна.
Я привел все аргументы «За». Но не услышал ни одного аргумента «Против».
Приведите пример хоть одной ситуации, где возникнут проблемы если в бизнес-логике использовать исключения.
Хотя бы производительность. Исключения принося некоторое удобство потребляют в несколько раз больше ресурсов. Исключение — нетипичная ситуация для нормального хода выполнения программы. Если условие еще как-то можно заоптимизировать, то с ислючением все сложнее.

Вот пример, сравнивающий скорость работы исключений с классами ошибок.
pastebin.com/vxWNH1VD
Хотя разница всего в 2 раза. Можно пренебречь в принципе. Я не думаю, что будет принципиально есть время.
Я спросил про бизнес-логику.
1. Cначала оптимизируют ботлнеки.
2. APC
3. Есть еще кучу «микрооптимизаций» которые можно применить прежде чем станет заметна потеря производительности на исключениях
Еще пример. Используя исключение в бизнес логике вы как-бы говорите — если происходит вот-это, то все прекратить и сделать это. Такая ситуация может привести к ошибке, если нужно что-то сделать перед началом обработки ошибки. То есть вы используете не совсем явный переход к обработчику ошибки.
переход явный throw new baseException(); — после этого ничего не выполняется в текущем методе.
Вы же не ожидаете, что что-то выполнится в функции после return;?
ретурн не пытается прервать также и вызвавшие их функции
Это бизнес логика тут надо быть внимательным. И если метод бросает исключение и нужно, что бы в месте вызова не прошло исключение значит его надо обработать там же.
8. вы спрашиваете что-то вроде «как мне быть, если мне действительно нужно забить гвоздь микроскопом?» или даже «как мне быть, если мне нужно рассмотреть микропрепарат кожицы лука с помощью молотка?»
так вот, вам не нужно передавать такое сложное сообщение исключением. они не для этого предназначены!

потом, у нас есть baseException / dbException / dbConnectException / lowBalanceException / fileNotFoundException. вам нужно, допустим, какое-то исключение переводить — например, fileNotFoundException. которое, напоминаю, согласно пункту 2 должно бы наследоваться от fileException.
соответственно, теперь либо вам придётся дублировать код из translateException в fileNotFoundException, либо вся иерархичность идёт к чертям и вы наследуетесь от translateException. да, можно переделать fileException в интерфейс, добавить класс baseFileException, заменить все инстанциирования fileException на инстанциирования fileBaseException, и продублировать внутренние переменные из fileException, если они там были, в fileNotFoundException.
Вопрос реализации решается легко: полиморфизм решаем интерфейсами (в интерфейс переделываем translateException — iTranslateException), дублирование кода минимизируем вынесением кода в статический метод, а потом будем его вызывать из не статических функций класса. Проблемы наследования надо решать по месту. Этот Exception может генерироваться на уровне контролеров, а не модулей, тогда проблема совсем не актуальна.
Вы не ответили на вопрос как мне передать такое сообщение. Вас спросили:«Доктор, у меня болит голова когда я лежу на левом боку. Что делать?». Вы ответили:«Не лежите на левом боку».
вы меня спросили «Доктор, у меня болит голова когда я подолгу стучусь ей об стену чтобы вызвать призрак Дональда Кнута. Что делать?»
а я вам и отвечаю: призрак Дональда Кнута вызывается с совсем другим способом, специальной командой в emacs, кажется, C-x M-c M-donaldknuth. а голову мучить не нужно.

для того, чтобы обрабатывать различные неисключительные ситуации и использовать exceptions для выдачи ошибок пользователю, вы предлагаете костыль в виде translateException. чтобы этот костыль соответствовал вами же декларированному принципу иерархичности, вы предлагаете второй костыль в виде iTranslateException и статического метода, который нужно вызывать из нестатических функций класса. и ещё говорите, что «Проблемы наследования надо решать по месту» — читай, проблемы на месте решайте костылями.

мне кажется, что если даже простые абстрактные задачи порождают такое количество костылей, значит, инструмент выбран неправильно. значит, для выдачи ошибок пользователю не нужно использовать исключения.
Хорошо, этот exception генерируется только в контролерах или только в одном модуле — проблема с наследованием решена. Или сам метод перевода убрали во внешнюю часть — обработчик на уровне view. Но мне до него все равно нужно донести шаблон текста ошибки и переменные. Как вы предлагаете решить эту задачу.
я не очень понимаю вопроса («этот exception» — это какой?) и не понимаю постановку задачи. может быть, вы всё-таки приведёте что-то менее абстрактное?
Товарищи, вижу без конкретных примеров реализации вы до утра (по мск точно) здесь будете словами перекидываться :-).
Этот exception это translateException. Задача — передать сообщение с ошибкой (которое можно перевести и в нем есть вариативные части) из кода к месту, где ошибка выводится пользователю.
Рассмотрим пример с игрой. Есть метод для создания игрового предмета из имеющихся материалов. Этот метод делает разные проверки, наличие предметов, маны, свободного места в рюкзаке итп. При неудачной попытке создания предмета, нужно показать пользователю сообщение следующего вида «У вас недостаточно маны(13) нужно 15» итп. Это сообщение нужно выводить на разных языках в зависимости от настроек пользователя. Соответственно этот метод может возвращать true в случае успеха или объект(ассоциативный массив) с шаблоном текста ошибки «У вас недостаточно маны(%d) нужно %d» и набор из двух переменных. Для того, чтобы показать, это сообщение нужно будет сначала в таблице переводов, найти шаблон ошибки в нужном языке, а потом уже подставить переменные. Фактически этот метод с кастомным механизмом ошибок. Можно не плодить такие кастомные механизмы, а использовать Exception, который в себе будет содержать всю необходимую информацию, как содержит ее в себе объект возвращаемый методом.
Если, подвести итог, то я не настаиваю на таком подходе, в конце концов, этот выбор каждый разработчик делает для себя сам.
это не исключительная ситуация. это бизнес-логика.

если не хватает не только манны, но и интеллекта, то нужно вывести:
— вам не хватаем манны
— вам не хватает интеллекта
чтобы вывести оба сообщения тебе придётся кидать не исключения LowMannaEx и LowIntEx, а абстрактное canNotMakeItem

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

Ну это будет внутри кода функции которая бросает исключения. Я проверю ману, если не хватает маны, то я проверю есть ли артефакт для пополнения маны, если он есть тогда пополняем (мы работаем в транзакции), если его нет бросаем исключение «Нет маны», в конце функции ловим исключение делаем откат и пробрасываем в исключение дальше.
и про «не хватает интеллекта» игрок узнает только когда наберёт нужное количество манны? х)
Я не внимательно прочитал условия. Если цель стоит агрегация ошибок, тогда нужно собрать ошибки, например, в массив и в конце бросить исключение со всеми ошибками внутри.

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

ты предлагаешь сделать по уму безо всяких исключений и в конце метода бросить исключение, которое тут же словить на следующием уровне? х) это извращённый способ возврата значения
я бы, кстати, предложил бы kotiar'е пойти дальше и использовать exception для возврата не только ошибок и неудач, но и для возврата положительного результата.
if (mana>10 && money>20) throw new OkExceptionTranslated(«всё ок!»);
для того, чтобы можно было не выдумывать специальный механизм для перевода сообщения об успехе.
теперь у нас всё работает через exceptions!!!
соответственно, если вдруг функция что-то вернула, то это следует воспринимать как ошибку, поскольку нормальные функции теперь должны кидать исключения.
итого,
function useArtefact($player, $artefact) {
    try {
        $artefact->use();
        throw new AppErrorExceptionTranslate("мы не должны были здесь оказаться");
    } catch (OkExceptionTranslate $ok) {
        $player->money -= $artefact->price;
        try {
            $player->save();
        } catch (OkExceptionTranslate $ok) { //удачно сохранили состояние игрока в БД
            throw new OkExceptionTranslate("артефакт использован круто");
        }
    } catch (DbException $e) {
        throw new FailExceptionTranslate("ошибка повторите через 5 минут");
    } //catch (MoneyException $e) { - MoneyException b ManaException пусть пробрасываются дальше
    throw new AppErrorExceptionTranslate("мы не должны были здесь оказаться");
}


в принципе, нормальный подход, чо. пишите свою CMS!!! победите битрекс и джуумлу!!!
Может быть, я где-то утрировал.
Все-таки бывает, что бизнес логику удобно сделать через исключения, а бывает и неудобно.
Делать надо по уму. Так что бы код было легко читать и поддерживать.
100% правил не бывает здесь делай так, а здесь вот так. Есть общие принципы и практики проверенные временем, но голову на плечах они не заменят.
Если немного погуглить, то как раз и находится общий принцип — «NEVER use exceptions for normal program flow logic». Я, правда, не до конца его сам понимаю, но наверное в нем что-то есть :).
Что вы скажите про такой вариант pastebin.com/6Z9irna3? Логика перевода вынесена из класса исключения в метод для view; интерфейс сведен к минимуму, так что повторения кода не значительны. Сам Exception служит контейнером для переноса сообщения и переменных.
Дональд Кнут вообще-то жив еще.
… а в emacs на самом деле нет команды C-x M-c M-donaldknuth.

да, спасибо, я в курсе!
Хм, а я вот формы валидирую бросая исключения, и выводя их сообщения пользователю :)
Вот кстати да, тут выше пишут что исключения нельзя применять для реализации бизнес логики, а тот же symfony в form framework исключения при валидации использует.
То, что пользователю, в большинстве случаев, знать о них не надо, не значит что их нет. Посмотрите в код любого из стандартных валидаторов. Вот, например, sfValidatorChoice:

if (!self::inChoices($value, $choices))
{
throw new sfValidatorError($this, 'invalid', array('value' => $value));
}
и использовать его предполагается так?

try {
$validator= new sfValidatorChoice(..)
$validator->validate()
// doSomeThing()
} catch( ValidatorException e ){
// doSomeThingElse()
}

вместо

$validator= new sfValidatorChoice(..)
if( $validator->isValid() )
// doSomeThing()
} else {
// doSomeThingElse()
}

? умно…
Простите, о чём Вы? symfony внутри form framework использует исключения для реализации основной логики, речь об этом.
Ну Вы бы посмотрели как всё там устроено, по моему мнению вполне разумно.
у них совмещена фильтрация с валидацией. но из-за этого они могут лишь сказать. что «поле не валидно», но не смогут «слишком длинное значение; введите хотябы одну цифру; пробелы использовать нельзя»
if ($this->hasOption('min_length') && $length < $this->getOption('min_length'))
{
throw new sfValidatorError($this, 'min_length', array('value' => $value, 'min_length' => $this->getOption('min_length')));
}
Что «остальные два»?
" введите хотябы одну цифру; пробелы использовать нельзя"
Ну ей богу, Вы бы потрудились сначала хоть чуточку разобраться о чём вообще идёт речь. В симфони стандартные валидаторы покрывают именно стандартные случаи, однако при желании всегда можно объявить свой валидатор со своими правилами и задать собственный тип ошибки, не min_length а например no_digits или spaces_not_allowed, а затем задать сообщение, ассоциированное с данным типом ошибки.
ошибки должно быть 3 одновременно, а не по очереди
Обрабатывать бизнес логику с помощью исключений считаю вполне приемлемым, получается очень читаемый код (т.к. ветвления убрали), только для этого нужно создать отдельную иерархию исключений.

Немного абстрактный пример кода:

try {
//
// редактирование профиля
//
} catch (AuthException $e) {
// надо быть авторизированным
} catch (AccessException $e) {
// надо иметь доступ
} catch (ValidationException $e) {
// надо правильно заполнять форму
} catch (ApplicationException $e) {
// таки shit happens
}

Вот и я о том же.
При такой модели работы, если нужно передать в обработчик ошибок дополнительную информацию (кроме типа исключения). Тут и пригодится пункт 5 — исключения это объект сам по себе и его можно расширять. И не нужно изобретать дополнительные грабли.
На мой взгляд, стоило упомянуть о возможности отлавливать исключения по типу.
На официальном сайте есть готовый и интуитивно понятный пример использования нескольких блоков catch для одного try.
Разумеется, многие языки поддерживают подобные конструкции.
Пересказывать мануал в статье бессмыслено. Я хотел рассказать то, чего там нет.
Что-то действительно как-то очень кратко сказано про исключения, как уже указали выше — не сказано про несколько блоков try catch, а они например добавляют понимания зачем наследоваться от стандартного класса исключений:
try {
  $object->method();
} catch(InvalidArgumentException $exception) {
  error_log('Переданы неверные аргументы');
} catch(LogicException $exception) {
  error_log('Логическая ошибка подпрограммы');
}
При этом если InvalidArgumentException отнаследован от LogicException — то до блока обработки LogicException дело не дойдет, потому как блоки исполняются поочередно, до тех пор пока не будет исполнен хотя-бы один, или пока не закончатся блоки, и проверка типа в них полностью аналогична instanceof.

И насчет глобального обработчика исключений — хрен с ним с обработчиком, без обработчика исключений никуда не денутся, а вот как действительно не надо делать — это:
} catch(WeAreDoomedException $exception) {
  // когда-нибудь надо дописать обработку
}
Да вы правы, насчет примера с WeAreDoomedException. Это еще один антипаттерн работы с исключениями. В таких случаях надо хотя бы логер ставить.
Assertion и исключения — совершенно разные вещи
Правильно. Я предлагаю бросать исключения в случае не прохождения утверждения (assertion fail)
(даже вот так)
в частности, вот:

The availability of exceptions may encourage developers to throw them when they are not appropriate or recover from them when it's not safe to do so. For example, invalid user input should not cause exceptions to be thrown. We would need to make the style guide even longer to document these restrictions!
Ну почему в С/С++ не используют исключения это понятно — экономят память. Потому как любое исключение по пути собирает на себе весь стек вызовов. Но С/С++ актуален для low-lewel вещей. Не надо путать теплое с мягким
удивительно, но среди 5ти пунктов, приведённых в документе, вы взяли кусочек одного из этих пунктов и написали, что «это понятно», написав в доказательство какую-то странную причину.
Документ, я не читал потому что, ваш второй комментарий я увидел позже, после написания своего. А причина такая, что не нужно пытаться натянуть принципы для С++ на PHP. C++ это низкоуровневый язык, вы же не будете писать на нем повседневные интернет приложения. (Хотя, можно написать отдельные высоконагруженые куски).
Следуя тому же документу, там предлагается 5 за и 5 против, и из 5 против к PHP подходит один два. И выбор они делают против исключений, только потому, что сложность работы с исключеними в С++ гораздо больше позитивных моментов. В PHP не так сложно работать с исключениями.
С другой стороны, насколько я понимаю, вы не против исключений, так как говорите:
> но я не предлагаю работать без исключений. я предлагаю считать исключительной ситуацией падение БД
Так что, я не понимаю, зачем вы опубликовали ссылку на документ в котором предлагают отказаться от исключений совсем.
Еще можно добавить (п. 4?), что в последнем catch всегда должен быть системный Exception. И обязательно в последнем, иначе другие никогда не смогут быть перехвачены.
Системный лучше обрабатывать на самом верхнем уровне (хотя это зависит от кода). Про порядок следования, написано в мануале
>что в последнем catch

А я что по вашему написал ;-)?
> на самом верхнем уровне
в смысле на самом верхнем уровне приложения — например в бутстрапе
Не только. Там один catch, это да.

Я про нисходящую обработку. Распространенные исключения вверху, дефолтный внизу. Это вообще-то правило. Никто не застрахован от того что «мы предполагали что он лишний и до него кто-нибудь перехватит исключение».
В яве этот вопрос имхо неплохо продуман, все исключения кроме ErrorException нужно ловить по факту, высылает метод исключения — значит нужно обернуть вызов в try/catch, если нет возможности обработать исключение на месте — нужно его выслать заново, но при этом вызывающий метод опять-же придется обернуть в try/catch, так как он в свою очередь может выслать исключение. Ну да не последнюю роль играет то, что там исключения указываются в заголовке метода.
Это по-моему правильный подход, несмотря на то что интерпретатор php в этом плане позволяет свободу — эта свобода выходит боком, исключения обычно высылаются без задней мысли о том кто их будет отлавливать, однозначно сказать на каком уровне будет обработано исключение, и будет-ли обработано вообще, нельзя.
Документирование кода (док блоки) и правильная IDE помогают, но это конечно не панацея
Ну помогают конечно, но все-же phpdoc это комментарии к коду, они носят уведомительный характер, если метод будет высылать исключения в нем не указанные — интерпретатор об этом не сообщит, как впрочем не сообщит если метод с указанными @throws будет вызван без try/catch, так что теоретически можно добиться того-же самого, но на практике это требует личных усилий программиста.
А не влияет ли негативно на производительность и скорость выполнения обильное бросание исключений в PHP?
Безусловно исключения будут влиять и на скорость выполнения скрипта, и на потребляемую им память, но реально это будет заметно когда их действительно много, если за время выполнения скрипта возникает несколько сотен/тыщ/мульонов исключительных ситуаций — то наверное производительность при этом должна волновать в последнюю очередь :)
> Преобразуйте все ошибки утверждений (assertion fail) и не фатальные ошибки в исключения
> Правильно. Я предлагаю бросать исключения в случае не прохождения утверждения (assertion fail)
Асерты работают только в режиме разработки и служат исключительно для тестирования и отладки кода, т.о.
1) Они НЕ предназначены для обработки ошибок и не могут возникнуть на продакш сервере
2) Переводить их в исключения — нелогично, т.к. это сильно усложнит обработку ошибок + это пустая трата времени (см. п.1)

На хабре, помню, асерты обсуждались очень подробно, но что-то не смог найти :(.

Касательно «не фатальные ошибки» — тоже не согласен. Вы забываете, что не фатальная ошибка (предупреждение, нотис и д.р.), в отличие от исключения НЕ приводит к прерываю работы, т.е. она позволит продолжить работу скрипта и вывести пользователю какие либо данные. Исключение же потребует реализации логики для вывода ошибки и вывода чего-то пользователю. Ну а реагировать на нотис исключением вообще глупо (скажем, не найдена строка локализации — думаете показать пользователю ошибку что её нет лучше, чем просто вывести страницу без неё?)

Добавлю еще один пункт:
X) ВСЕГДА указывайте какие исключения выбрасывает метод (@throws) — в будущем сильно сэкономите время себе и поможете другим разработчикам.

Без последнего пункта весь ваш код со временем выродится — все try блоки будет ловить только базовое исключение, т.к. разработчикам будет лень рыться в чужом коде и искать какие исключения он выбрасывает.
Все равно assertion как-то обрабатывать надо, чтобы не изобретать велосипед используем готовую модель. Если где-то assertion не пройдет получаем исключение, а с ним и trace. Тут кому как больше нравится.

Насчет преобразования ошибок, тут надо понимать какая этому альтернатива. Рассмотрим ваш пример
> скажем, не найдена строка локализации — думаете показать пользователю ошибку что её нет лучше, чем просто вывести страницу без неё?
если у вас задумана логика, что если строки локализации нет, то вывести то что есть, так и напишите это в коде:
if(!array_key_exists($message, $translate)){
    return $message;
} else {
    return $translate[$message];
}

представьте себе ситуацию, если вы не проверяете наличие ключа в массиве, этот не существующий элемент массива будет преобразован в null, пустую строку, 0, или false. В общем случае такое поведение может нарушать задуманную логику приложения. При моем подходе мы получим исключение, да операция закончится неуспехом, но мне кажется это безопасней, чем приложение которое пойдет по не предвиденной ветке логики. Но если в коде стоит проверка, то и мой вариант и вариант без преобразования ошибок в исключения отработает одинаково.
> Все равно assertion как-то обрабатывать надо, чтобы не изобретать велосипед используем готовую модель. Если где-то assertion не пройдет получаем исключение, а с ним и trace. Тут кому как больше нравится.

Единственная обработка которая нужная при срабатывании асерта — это исправление ошибки (на дев сервере, на рабочем он все равно никогда не сработает).

> если у вас задумана логика, что если строки локализации нет, то вывести то что есть, так и напишите это в коде:
Так нельзя делать. Обнаружить пропущенную строку при таком подходе невозможно. Это как раз тот случай когда больше всего подходят E_NOTICE (или E_USER_NOTICE).

> представьте себе ситуацию, если вы не проверяете наличие ключа в массиве, этот не существующий элемент массива будет преобразован в null, пустую строку, 0, или false.

Это больше относится к стилю программирования, чем к обработке ошибок… Все нотисы оседают в логи (нотисы я считаю ошибками и при нормальной работе их быть не может) достаточно посмотреть их чтобы обнаружить пропущенную строку (индекс и т.д.). Ну, а на дев сервере нотисы сразу выводятся вместе с трассой (спасибо xdebug) и исправляются при обнаружении.

Зачем здесь исключение?

+ Есть еще E_DEPRECATED/E_USER_DEPRECATED на который вообще нельзя бросать исключение, т.к. код полностью рабочий хотя и устаревший.

+ Часть стороннего кода (в большом проекте он в большинстве случаев будет) может не считать E_NOTICE ошибкой, в вашем случае потребуются костыли чтобы заставить его работать.
> Обнаружить пропущенную строку при таком подходе невозможно.
Если вам еще дополнительно, нужно искать непереводимые строки, добавляем код, который логирует
if(!array_key_exists($message, $translate)){
    log($message, 'untranslated string');
    return $message;
} else {
    return $translate[$message];
}
 

> Ну, а на дев сервере нотисы сразу выводятся вместе с трассой (спасибо xdebug) и исправляются при обнаружении.
А если это какой-то сложно воспроизводимый баг и на дев сервере вы его не нашли. Отдали код в продакшин. Там на него натыкается конечный пользователь. Вы ищете этот notice среди не очень информативных сообщений в логе PHP и даже если вы его нашли, не факт, что станет понятно как его воспроизвели. В кастомном логере исключения может быть как минимум trace, так же можно залогировать страницу на которой это произошло, глобальные переменные (если это поможет) ит.п.

Насчет стороннего кода вы правы, могут быть проблемы. Варианты решения
костыль 1. ставим собачку при инклюде стороннего кода и выключаем режим screem
костыль 2. определяем сторонний код по названию файла или класса в errorHandler с помощью debug_backtrace()
решение 3. отказываемся от такого стороннего кода (если есть возможность)
решение 4. отказываемся от такого подхода работы с ошибками (т.е. от моего варианта) — в конце концов надо решить задачу, а не следовать всем правилам
решение 5. если код больше не поддерживается другой командой — достался вашей команде, то можно отрефакторить где надо
Нотисы обязательно нужно переводить в исключения во время разработки. Я для этого держу специальный конфиг, который подключает котеровский перехватчик в разработке, и не подключает на продакшне.

Я видел примеры, когда из-за ошибок программера могли потеряться немалые деньги, если бы код не был перепроверен с выводом нотисов.
12. Давай по лицу за названия классов с маленькой буквы.
Очень простое правило, при использовании исключений: если можно проверить успешность операции до начала операции исключение использовать не стоит. В большенстве таких случаев это т.н. «костыли». Падени БД, файловой системы, обрыв связи — вот исключительные ситуации. Всё остальное — ошибки. Либо программистов либо пользователей. Соответсвенно — ассерты или обюработка ошибок.
UFO just landed and posted this here
UFO just landed and posted this here
Правило 1 — если где-то в коде бросается системное исключение (Exception), то и ловить его придется так
try{
    //...    
} catch(myException $e) {
    //...
} catch(Exception $e) {
    //здесь вы можете поймать все, что не поймали раньше
}

получается, что вся сложная иерархия исключений идет лесом, так как все исключения скопом поймает все, которые не были указаны в списке выше.
Пункт 4 — глобальный обработчик нужен для того, чтобы обрабатывать все не пойманные ранее исключения и обработать их хоть как-то — вывести сколь нибудь понятное сообщение пользователю, залогировать их или отослать письмо разработчику.
UFO just landed and posted this here
При таком подходе я бы вам не советовал пользоваться исключениями.
Пункту 4 так же справедлив, если возникает не предвиденная ранее ошибка (которую не предусмотрел разработчик и не увидел тестеровщик) в продакшине. В вашем коде никогда не находили баги в продакшине? Так во первых можно будет свое временно узнать об ошибке, будет откуда начать поиск в виде trace исключения в логе.
UFO just landed and posted this here
> Я считаю, что указание на вид ошибки должно размещаться не в названии класса исключения, а в свойстве класса
Взято отсюда
Любой объект уже обладает типом. Этим типом является его класс. Внедряя информацию о типе в сам класс, вы можете нарушить принцип открытия/закрытия…

В статье, ссылку на которую вы давали выше:
Как сделать лучшую архитектуру

Принцип открытия-закрытия (Open-Closed Principle) – классы должны быть открыты для расширений, но закрыты для модификаций. Кажется, что это невозможно, однако стоит вспомнить шаблон проектирования Strategy и становится более или менее понятно.


Не находите противоречий?
UFO just landed and posted this here
Это — полный ппц. А если мне только DBError поймать надо, для всех остальных случаев делать еще один throw? Кроме того, такие случаи нормально не задокументировать. А какого потом придется тому, кто с вашим кодом работать будет?

Есть механизм — исключения. Со всеми ключевыми словами, правилами и т.п и т.п. Не надо поверх этого механизма плодить свой говнокод, который потом будут с дикими матами разбирать другие разработчики. Ибо любой нормальный разработчик ожидает разные типы исключений для разных ошибок.
UFO just landed and posted this here
В юзабилити есть такой термин — user experience. Так вот любой программист, который пишет более чем на одном языке очень хорошо на своей шкуре знает что это такое. В программировании это когда в разных языках от схожих конструкций ожидается одно поведение. Хороший пример — Java и Javascript. PHP же многое заимствовал из других языков, но при этом везде есть свою нюансы, из-за которых очень часто хочется материться. Отличный пример — символ разделения пространства имен.

Тем не менее, механизм исключений в PHP совпадает со «стандартом», поэтому любой программист PHP, JS, C++, Java и огромной кучи других языков, видя try-catch или throw ожидает одно и то же.

Я бы мог еще написать про возможности нормальных IDE по работе с классами, про анти-паттерн God Object, про то, что KISS — это не меньше классов, а простая архитектура и про кучу других вещей, но боюсь, что все это будет бесполезно. Очень надеюсь что ни мне, ни моей команде никогда не придется работать с вашим кодом.

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

У вас есть свое представление о красивом коде. Да без проблем! Но, вы, видимо, из тех разработчиков, что считают написание кода важнее бизнес-процессов вашего заказчика, пусть даже вы сами и есть этот заказчик. Каждый программист проходит через этап когда собственная важность кажется огромной, но не каждый его перерастает.
UFO just landed and posted this here
Первые два абзаца, как раз, содержат больше всего смысла.

> Но подстраиваться под IDE — глупо
Это экономит время. Написание того же самого phpdoc для самого себя — это экономия времени, т.к. IDE его подхватывает.

> Я из тех разработчиков, которые считают, что производительность кода важнее того, насколько удобно и красиво было его писать.
Епт! Давайте еще порассуждаем что быстрее — print или echo? Просто цитата: «Premature optimization is the root of all evil»

> Классы ConnectorDBWrongPassword, ConnectorDBWrongLogin, ConnectorDBWrongHost — это кунсткамера какая-то
Ну так не надо в крайности впадать. Классов HostNotFoundException и DBConnectionException хватит за глаза. Причем первое исключение должно бросаться во многих случаях, а не только по отношению к базе.

> public __construct ([ string $message= "" [, int $code= 0 [, Exception $previous= NULL ]]] )
Вот именно поэтому в той же самой Java нету параметра code. Ибо он нафиг не нужен.

 

На самом деле после фразы «производительность кода важнее того, насколько удобно и красиво было его писать» что-либо обсуждать мне расхотелось. Либо с вашей стороны это был тонкий троллинг и тогда могу только вас поздравить, либо, уж извините за прямоту, я не считаю вас адекватным разработчиком, чтобы тратить время на дальнейшие споры, которые мне пользы не принесут.
UFO just landed and posted this here
Начали за здравие (agiledev, S.O.L.I.D), а закончили за упокой (php manual как эталон дизайна).
UFO just landed and posted this here
ты явно не программировал на яваскрипте х)
любой нормальный разработчик сначала читает доки, а потом что-то ожидает.
Я очень не завидую тем, кто вас нанимает.
UFO just landed and posted this here
любой объект обладает типом и не одним.
например 4 принадлежит к классу/типу чётных и к типу/классу положительных.

почему я могу перехватить все исключения вызываемого мной модуля, но не могу перехватить все исключения связанные с файловой системой?

пусть у нас есть модуль А, он использует модули Б, Ц и Д, каждый из которых бросает однотипные исключения (например, не могу открыть файл), но каждый называет его по своему. поэтому модулю А приходится их перехватывать и заворачивать в своё «не могу открыть файл». в результате у нас получается 4 имени для одного и того же исключения. зачем?

а что если нам нужно перехватывать «проблемы с файлами» и «проблемы с базой» и для этих случаев выполнить один и тот же код, а для других проблем — другой? синтаксис catch( ExcaptionName $e ) тут уже не рулит, ибо эта задача не вписывается в «единую иерархию»
пусть у нас есть модуль А, он использует модули Б, Ц и Д, каждый из которых бросает однотипные исключения (например, не могу открыть файл), но каждый называет его по своему. поэтому модулю А приходится их перехватывать и заворачивать в своё «не могу открыть файл». в результате у нас получается 4 имени для одного и того же исключения. зачем?

Если в модули Б, Ц и Д бросают однотипные исключения, они делают однотипные операции и нужно задуматься о рефакторинге:
или свести все три модуля к одному или вынести общую часть в один К, а модули Б, Ц и Д будут его использовать; если речь идет о классах, то общую часть можно вынести в один общий родительский класс.
И тогда бросаемое исключение будет «кИсключение».
Если Б, Ц и Д это классы наследуемые от К и нам нужно иметь возможность: как поймать все «кИсключения» разом так и ловить «бИсключения», «цИсключения» отдельно, тогда просто нужно объявить их потомками «кИсключения».

Если же вдруг вам все равно мало возможностей иерархии(дерево), то такой полиморфизм решается интерфейсами.
это сторонние _независимые_ модули.

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

почему я могу перехватить все исключения вызываемого мной модуля, но не могу перехватить все исключения связанные с файловой системой?

можете catch(fileModuleException $e)

пусть у нас есть модуль А, он использует модули Б, Ц и Д, каждый из которых бросает однотипные исключения (например, не могу открыть файл), но каждый называет его по своему. поэтому модулю А приходится их перехватывать и заворачивать в своё «не могу открыть файл». в результате у нас получается 4 имени для одного и того же исключения. зачем?

ну правильно, чтобы было меньше зависимостей, чтобы в коде оперировать только исключениями модуля А. 4 имени — и что в этом плохого? Использовать в коде надо только одно исключение модуля A

а что если нам нужно перехватывать «проблемы с файлами» и «проблемы с базой» и для этих случаев выполнить один и тот же код, а для других проблем — другой? синтаксис catch( ExcaptionName $e ) тут уже не рулит, ибо эта задача не вписывается в «единую иерархию»

если очень нужно, можно сделать через интерфейсы
interface myDbFileException {}
class fileException extends baseException implements myDbFileException{}
class dbException extends baseException implements myDbFileException{}

UFO just landed and posted this here
3. rollback сам может кинуть исключение и тогда мы потеряем исходное
Мой метод не предполагает бросание исключений, если у вас в методе предполагается бросание исключений, то его не стоит использовать в catch.
а как же потеря коннекта к базе?
Значит, rollback() надо тоже обернуть в try-catch
ага, и вообще все вызовы методов надо заворачивать в try-catch ;-)
заворачивать надо там, где собираетесь обработать исключение
дык вот, обработал, получил два исключения. какое из них должно всплыть?
А какое важнее?
Из-за чего может возникнуть исключение в rollback(), я вижу две причины:
1. отпал коннект к базе, и соответственно транзакция сама откатится по таймауту
2. конец базе
В первом случае ничего страшного не произошло, второй случай — за жизнью базы должен следить отдельный человек/механизм, а не исключения в ролбеках. Вот и получается, что внутри роллбека, нужно заглушить и залогировать исключение.
А работать нужно с первым исключением, которое может нести в себе информацию для вывода на экран.

В ролбеке исключения нужно заглушить, но при этом комит может и даже должен бросать исключения (в случае возникновения исключительных ситуаций).
и использующее этот класс приложение никаким образом не сможет обработать исключительную ситуацию, потому что исключение мы скушали. супер.
Может быть мы друг друга не понимаем?
Я же написал, почему такое исключение никак обрабатывать не надо (разве что залогировать).
Если rollback будет бросать исключения, то это сделает не возможным (не удобным) его использование в catch.
Да собственно как вы будете его обрабатывать? Коннект к базе уже отвалился и по таймауту транзакция и так откатится. Если вам нужно знать, что rollback не прошел верните false или так.
а глотание ошибок делает невозможным(не удобным) его использования в try

представь, что у тебя своя объектная бд на файлах и мемкешах. и rollback — это уже не тупой адаптер к серверной ручке, а целое мини-приложение. а ты ошибки глотаешь…
если этот адаптер не умеет откатывать транзакцию по отпадению конетка (приложения), то глотание ошибок не самая большая проблема
ага, если ваше приложение не может выполнить поставленную задачу, то выдача пользователю пустой страницы — не самая большая проблема х)
Исключения должны проектироваться не от кода, а от ситуаций,
которые вы хотите по-особенному обработать.

Исключения являются частью внешнего интерфейса. Т.е. это то, с чем будет
взаимодействовать _клиенсткий_ код (чужой — а не ваш). Вы не можете заранее предугадать, какие ситуации клиентский код «захочет»
обрабатывать «по-особенному». Соответственно — проектировать исключения исходя
из какой-то особенной обработки нет ни какого резона. Вы можете лишь обозначать
исключительные ситуации в той мере, в которой сочтете нужным
это требуется для обеспечения полноты модели предметной области.
Проектирование исключений, это как проектирование API, чем больше хуков заложено, тем больше возможностей у третестороннего разработчика, и соответственно больше времени потратится на разработку и система станет тяжелей (больше классов). Это выбор каждого разработчика, кто как считает правильным в том или ином контексте.
UFO just landed and posted this here
Напоминаю, что в прошлой статье грустно и одиноко висит без ответа мой вопрос про коды возврата и «лесенку» ^_^
ага, и мой пример с артефактом манны ._.
А что по поводу обработки fatal все-таки? У меня любая попытка кинуть исключение при обработке fatal error приводит к немедленному завершению скрипта
fatal можно обработать если set_error_handler выполнился в не в том файле где случается фатал

Цитата с php.net
The following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT raised in the file where set_error_handler() is called.

Скорее всего дело в этом. Если нет, выложите пример кода.
Sign up to leave a comment.

Articles