singalen: (Default)
[personal profile] singalen
Джоэл ссылается тут и тут на статью Реймонда Чена, выступающую против исключений. Перечитать со всеми ссылками.
В общем, аргумент насчёт того, что при наличии exceptions отследить глазом все неявные переходы почти невозможно, верен, - если не принимать во внимание инструментальную поддержку.
Java checked exceptions, увы, не имеют аналогов в мейнстриме (как я его вижу; читай - C#, C++). Если рассматривать только код в нотепаде, то да, но - у нас есть IDE и компилятор, который сильно облегчает поиск проблем с checked exceptions. Возможен и компилятор C++, выдающий warnings по образу compile-time ошибок Java с checked exceptions.
С третьей стороны, просто читая код, так-таки не увидишь этих "неявных goto".
Надо посчитать, насколько предупреждения/ошибки компилятора о checked exceptions помогают отличить правильный код от неправильного.

Upd: Добавляю свой старый конспект с нашей локальной тусовки, которую наадеюсь продолжить с осени.

1. Checked vs unchecked exceptions.


Эпиграф.
... некоторый вещи остались "за кадром" (так произошло, в частности, с обработкой ошибок - вопросом, недостаточно хорошо проработанным мною...
"Корпоративные приложения" Мартина Фаулера, предисловие.

По пути к месту заседания обсуждалась следующая мысль: развитие средств разработки идёт по пути всё более строгого описания контракта класса (модуля, интерфейса, функции). В С можно было не указывать типы параметров функции, в С++ функция не обязана декларировать бросаемые исключения, в Java и С# упразднены указатели. C# сделал шаг назад, введя только unchecked exceptions - но это только чтобы не быть полностью слизанным с Java ;))

Эккель: One thing has become very clear to me, especially because of Python: the more random rules you pile onto the programmer, rules that have nothing to do with solving the problem at hand, the slower the programmer can produce. And this does not appear to be a linear factor, but an exponential one (http://www.mindview.net/Etc/Discussions/CheckedExceptions)
Примем этот постулат за аксиому.
Отсюда следует, что механизм проверки ошибок должен быть простым в обращении. Проще, чем АКМ.
Можно говорить, что "исключения требуют очень аккуратного и квалифицированного подхода в использовании". Да, это так. Но то же самое можно сказать о любом механизме, который является общепризнанным источником проблем - например, об указателях в C, или о кодах возврата функций,

Anders Hejlsberg (ведущий архитектор C#): I see two big issues with checked exceptions: scalability and versionability.
(http://www.artima.com/intv/handcuffs.html)

Проблема с versioning в том, что метод, в одной реализации не бросавший исключений, начинает бросать некое исключение. Нужно менять интерфейс предка, что обычно неудобно или невозможно: на интерфейс предка завязаны хрюкобайты кода, либо он попросту недоступен для изменения.
Проблема scalability - накопление в интерфейсе возможных throws при росте объёма системы. Он привел пример, когда их 4 десятка.
Ценная мысль его же: It is funny how people think that the important thing about exceptions is handling them. That is not the important thing about exceptions.
Нельзя не согласиться.

"С одной стороны, нельзя не признать, но с другой стороны, нельзя не согласиться". Серьёзный аргумент в пользу checked exceptions:
James Gosling: Lots of newbie's coming in from the C world complain about exceptions and the fact that they have to put exception handling all over the place-they want to just write their code. But that's stupid: most C code never checks return codes and so it tends to be very fragile. If you want to build something really robust, you need to pay attention to things that can go wrong, and most folks don't in the C world because it's just too damn hard.
One of the design principles behind Java is that I don't care much about how long it takes to slap together something that kind of works. The real measure is how long it takes to write something solid.
(http://www.artima.com/intv/solid.html)

То есть, даже если работа программиста замедлится в разы, но не будет ли это компенсировано качеством кода?

Пример от Сергея (TODO: сделать ссылку на блог Сергея, буде таковой имеется): singleton's getInstance() with lazy initialization. В этом случае getInstance() может бросать любые IOException, любимые SQLException, ХЕЗException... однако за getInstance(), который бросает IOException, надо бить. Такое исключение можно пробрасывать как Unchecked.
Пример можно распространить на любые утилитные компоненты из нижележащих layer-ов, в которых применяется lazy initialization. Таковы почти все они, начиная с печальноизвестных SQLException из JDBC.
С другой стороны, как дать знать клиенту, если в SomeClass.getInstance() ДЕЙСТВИТЕЛЬНО происходит IOException? Наверное, его таки-надо бросать?

Сергей описал ещё одну проблему (или аспект проблемы versioning) и предложил подход к её решению. Проблема заключается в том, что когда програмист пишет код, он сначала пишет его для сценария "полный успех". Как следствие, никаких try-catch и throws в коде нет.
И это, товарищи, правильно, потому что даже сценарий успеха может оказаться достаточно сложным, чтобы парить програмисту мозг ещё и всеми возможными исключениями вызываемых методов.
В большинстве случаев, написав код и торопясь его сдать, он просто добавит throws ... к хедеру функции, или, ещё хуже, напишет один на всю функцию try...catch (Exception) { printStackTrace() }.
С другой стороны, этот противный блок { printStackTrace() } будет живым напоминанием, что вот в этом коде ещё не предусмотрена обработка ошибок, которая ДОЛЖНА тут быть. См. James Gosling. Также см. Джоэла - это как раз и будет making wrong code look wrong.

Предложенное Сергеем решение: отсутствие поимки checked exception должно вызывать не ошибку компиляции, но warning. Либо, вызывать error или warning в зависимости от флагов компилятора.

Возможный подход: ограничить распространение исключения его abstraction layer-ом. Внутри layer-а декларировать все методы как бросающие исключени(е/я) этого уровня (возможно, даже те, которым это исключение не нужно: ведь если мы декларируем интерфейс, неизвестно, как именно он будет реализован). На следующий же уровень абстракции пробрасывать, оборачивая в "транспортное" исключение.

Обобщаю проблемы.
1. Заранее неизвестно, какое исключение бросит вызываемая функция.
2. С другой стороны, некоторые функции могут быть заранее помечены как бросающие исключение NoHammerException, и эту информацию надо использовать при компиляции вызывающего кода.
3. Вызывающий код должен а) обрабатывать явно задекларированные исключения, и б) увы, обрабатывать ещё и незадекларированные исключения, либо их надо заранее декларировать.
Следствие: вообще не чекать исключения, либо пробрасывать чекаемые, но не предусмотренные интерфейсом, через эккелевский адаптер (заворачивать в RuntimeException). И то, и то череповато ухудшением качества кода.

Варианты решения вопроса на уровне языка :)

1. Вынести обработку исключений в отдельный блок ЗА телом функции, как это делается в конструкторах C++:

int killRabbits() throws NoGunException, NoAmmoException
{
}
catch (NoGunException)
{
}
catch (NoAmmoException)
{
}


2. Ещё вариант покорежить язык Java согласно придуманным концепциям :)

int killRabbits() throws RabbitProcessingException as RabbitLayerException

где RabbitProcessingException - это общее исключение "работы с кроликами", предок NoGunException, NoAmmoException, а конструкция as RabbitLayerException обозначает, что исключения RabbitProcessingException должны быть автоматически завернуты в RabbitLayerException.

3. Ввести понятие "функции для обработки исключений", каковое мне, например, уже доводилось вводить и так, и вызывать её для определенных исключений.
Дело в том, что если разные методы одинаково обрабатывают NoGunException, то это не значит, что его на самом деле должен обработать вызывающий код. Вызывающим кодом может быть и управляющий тред фреймворка (если, как всегда, произошёл inversion of control), а уж ему-то наши исключения нафик не нужны.
Поэтому возникает функция, единообразно обрабатывающая NoGunException и NoAmmoException в некоем модуле, и несколько, извините за каламбур, исключений из этого правила. Согласитесь, исключения в разных функциях чаще обрабатываются единообразно, чем индивидуально?

int killRabbits() throws RabbitProcessingException catch RecoverRabbitError {...}
void RecoverRabbitError (RabbitProcessingException e) throws ... {...}


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

try
{
...
}
catch(RabbitProcessingException e)
{
RecoverRabbitError(e);
}

void RecoverRabbitError (RabbitProcessingException e) throws ERabbitLayerError {...}


где RecoverRabbitError - функция, состоящая только из обработчиков исключений.

Злоключение.
Исключения - сложный и малоизученный механизм :) Следствие - этот механизм нуждается в упрощении и улучшении изучабельности.
Обработка ошибок должна сочетать элементы выкручивания рук программисту "обработай меня обязательно!", но при этом быть ещё более отделённой от кода "успешного сценария".
Отделять сам по себе "успешный сценарий" от обработки ошибок - достаточно сложная задача

(no subject)

25/8/05 01:33 (UTC)
Posted by [identity profile] dpak0h.livejournal.com
mainstream у нас же java во всём мире, кроме российских m$-ориентированных просторов