Просто потому, что вы не можете видеть путь для ошибки, ещё не означает, что она не существует.
Вот кусок кода из книги по программированию, взятый из главы, где показывается, как замечательны исключения:
Чище, элегантнее и неверно.try accessDb := TAccessDatabase.Create; accessDb.GenerateDatabase; except on E: Exception do // Анализировать пойманное исключение end; ... procedure TAccessDatabase.GenerateDatabase; begin CreatePhysicalDatabase; CreateTables; CreateIndexes; end;Заметьте, насколько чище и элегантнее это решение.
Предположим, исключение возбуждается при выполнении CreateIndexes. Функция GenerateDatabase не ловит его, поэтому ошибка передаётся вызывающему, где её ловят.
Но когда исключение покидает GenerateDatabase, теряется важная информация: состояние создания базы данных. Код, который ловит исключение, не знает: какой этап в создании базы данных провалился. Нужно ли удалять индексы? Нужно ли удалять таблицы? Нужно ли удалить физическое хранилище базы данных? Он не знает.
Так что, если есть проблема при вызове CreateIndexes, то у вас навсегда утекают физический файл базы данных и таблицы (поскольку, предположительно это файлы на диске - они навсегда останутся там).
Написание правильного кода в модели с возбуждением исключений является в некотором смысле сложнее, чем в модели ошибка-код, поскольку любой код может потерпеть неудачу, и вы должны быть готовы к этому. В модели ошибка-код, очевидно, когда нужно проверять на наличие ошибок: когда вы получите код ошибки. В модели с исключениями, вы просто должны знать, что ошибки могут происходить в любом месте.
Иными словами, в модели ошибка-код, очевидно, когда кому-то не удалось обработать ошибку: они не проверяют код ошибки. Но модели с возбуждением исключений, это не очевидно из простого чтения кода: обработал ли кто-то ошибку - поскольку сама ошибка не является явной.
Рассмотрим такой код:
function AddNewGuy(const AName: String): TGuy; begin Result := TGuy.Create(AName); AddToLeague(Result); Result.Team := ChooseRandomTeam; end;Эта функция создаёт новый объект Guy, добавляет его в лигу и присваивает его случайной команде. Что может быть проще?
Запомните: каждая строка - это возможная ошибка.
- Что если исключение будет возбуждено "TGuy.Create(AName)"?
- Ну, к счастью, мы ещё ничего не начали делать, так что ничего страшного и не произошло.
- Что если исключение будет возбуждено в "AddToLeague(Result)"?
- Созданный объект Guy будет забыт (утечка ресурсов). В языках с GC (Garbage Collector - уборщиком мусора), этот объект будет удалён чуть позже.
- Что если исключение будет возбуждено в "Result.Team := ChooseRandomTeam"?
- Ой-ой, теперь у нас проблемы. Мы уже добавили guy в лигу. Если кто-то поймает это исключение, они обнаружат guy в лиге (группе команд), который не принадлежит ни одной команде. Если где-то есть код, который проходит по всем членам лиги и использует свойство guy.Team, то этот код схлопочет Access Violation, потому что свойство guy.Team ещё не инициализировано.
Окей, как же мы можем исправить это? Переупорядочив операции.
function AddNewGuy(const AName: String): TGuy; begin Result := TGuy.Create(AName); Result.Team := ChooseRandomTeam; AddToLeague(Result); end;Это, казалось бы незначительное, изменение имеет огромный эффект на восстановление после ошибки. Откладывая сохранение (commitment) данных (т.е. добавление guy в лигу), любые исключения, возникающие при создании и настройке guy, не будут иметь длительного эффекта. Всё, что может здесь произойти - частично-инициализированный guy утечёт (и потом будет очищен GC, если таковой есть в языке).
Общий принцип проектирования: не подтверждайте данные, пока они не будут готовы.
Конечно же, этот пример был довольно простой, поскольку шаги по настройке guy не имели побочных эффектов. Если что-то пошло не так при настройке - мы могли просто отказаться от экземпляра guy.
В реальном мире вещи намного более запутаны. Посмотрите на это:
function AddNewGuy(const AName: String): TGuy; begin Result := TGuy.Create(AName); Result.Team := ChooseRandomTeam; Result.Team.Add(Result); AddToLeague(Result); end;Эта функция делает то же, что и предыдущая исправленная, но только тут кто-то решил, что не плохо бы каждой команде иметь список входящих в неё членов, так что вы должны добавить себя в команду, если вы хотите в неё войти. Какие последствия будет это иметь на правильность функции?
Спасибо за переводы.
ОтветитьУдалитьРеально помогают держаться в тонусе.
>> Нужно ли удалить физическое хранилище базы данных? Он не знает.
ОтветитьУдалитьПочему не знает? Знает. Да, нужно удалять. Создание обломилось, результат некорректен.
>>> Почему не знает? Знает. Да, нужно удалять. Создание обломилось, результат некорректен.
ОтветитьУдалитьИмелось в виду, что надо ли отменять создание.
Вы же говорите про "удалять в любом случае, даже если БД не была создана".
Да, можно следовать подходу "отменять всё подряд", но он не всегда возможен.
Это и есть смысл этого примера: "теряется важная информация: состояние создания базы данных".
Вернее не совсем так. Ведь в данном случае правильное решение - отмена действий внутри GenerateDatabase и её подпрограмм, чтобы каждый вызов оставлял бы программу в согласованном состоянии. Поэтому пример показывает, что авторы сказали "смотрите как просто" и... совершили ошибку.
По мнению Реймонда, конечно.
>Александр Алексеев 31 октября 2011 г. 23:00
ОтветитьУдалитьА зачем клиенту GenerateDatabase знать детали реализации (что там есть какие-то индексы, таблицы, сепульки и фрэглы)?
Ну вот узнал клиент, что GenerateDatabase зафейлила потому что поляризация ротора дивергенции векторного поля квазистатического сепулькария конгруэнтна вчерашней погоде на Марсе. И что ему делать с этим сакральным знанием?
С одной стороны, клиент имеет инкапсулированный объект, а с другой -- на него вываливаются кишки деталей реализации.
> А зачем клиенту GenerateDatabase знать детали реализации (что там есть какие-то индексы, таблицы, сепульки и фрэглы)?
ОтветитьУдалитьВ том-то и дело, что не нужно.
Зачем под Делфи переделывать? о_О
ОтветитьУдалить