пятница, 27 марта 2009 г.

Чище, элегантнее и неверно

Это перевод Cleaner, more elegant, and wrong. Автор: Реймонд Чен.

Просто потому, что вы не можете видеть путь для ошибки, ещё не означает, что она не существует.

Вот кусок кода из книги по программированию, взятый из главы, где показывается, как замечательны исключения:
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;
Эта функция делает то же, что и предыдущая исправленная, но только тут кто-то решил, что не плохо бы каждой команде иметь список входящих в неё членов, так что вы должны добавить себя в команду, если вы хотите в неё войти. Какие последствия будет это иметь на правильность функции?

6 комментариев:

  1. Спасибо за переводы.
    Реально помогают держаться в тонусе.

    ОтветитьУдалить
  2. >> Нужно ли удалить физическое хранилище базы данных? Он не знает.

    Почему не знает? Знает. Да, нужно удалять. Создание обломилось, результат некорректен.

    ОтветитьУдалить
  3. >>> Почему не знает? Знает. Да, нужно удалять. Создание обломилось, результат некорректен.

    Имелось в виду, что надо ли отменять создание.

    Вы же говорите про "удалять в любом случае, даже если БД не была создана".

    Да, можно следовать подходу "отменять всё подряд", но он не всегда возможен.

    Это и есть смысл этого примера: "теряется важная информация: состояние создания базы данных".

    Вернее не совсем так. Ведь в данном случае правильное решение - отмена действий внутри GenerateDatabase и её подпрограмм, чтобы каждый вызов оставлял бы программу в согласованном состоянии. Поэтому пример показывает, что авторы сказали "смотрите как просто" и... совершили ошибку.

    По мнению Реймонда, конечно.

    ОтветитьУдалить
  4. >Александр Алексеев 31 октября 2011 г. 23:00

    А зачем клиенту GenerateDatabase знать детали реализации (что там есть какие-то индексы, таблицы, сепульки и фрэглы)?
    Ну вот узнал клиент, что GenerateDatabase зафейлила потому что поляризация ротора дивергенции векторного поля квазистатического сепулькария конгруэнтна вчерашней погоде на Марсе. И что ему делать с этим сакральным знанием?
    С одной стороны, клиент имеет инкапсулированный объект, а с другой -- на него вываливаются кишки деталей реализации.

    ОтветитьУдалить
  5. > А зачем клиенту GenerateDatabase знать детали реализации (что там есть какие-то индексы, таблицы, сепульки и фрэглы)?

    В том-то и дело, что не нужно.

    ОтветитьУдалить
  6. Зачем под Делфи переделывать? о_О

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.