Обеспечение поведения отката

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

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

В мире С++ популярное решение этой проблемы подразумевает разработку вспомогательного класса, экземпляр которого создается в стеке.

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

Трюк кроется в деструкторе. Если деструктор выполняется перед тем, как флаг выставлен, существуют только две возможности. Первая — когда пользователь просто забыл вызвать Commit.

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

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

Если через исключение, то все, что понадобится сделать — это вызвать Rollback на объекте базы данных, и получается необходимая функциональность.

Все это прекрасно в мире С++, где можно воспользоваться детерминированной деструкцией. Однако того же конечного результата можно достичь с применением формы детерминированной деструкции С#, представляющей собой сочетание IDisposable и ключевого слова using.

Вспомните, что деструктор в “родном” С++ отображается на интерфейс IDisposable в С#. Все, что потребуется сделать — взять код, который был бы помещен в деструктор в С++, и включить его в метод Dispose вспомогательного класса С#.

Посмотрим, как может выглядеть такой вспомогательный класс С#:

using System;
using System.Diagnostics;
public class Database {
public void Commit ()  {
Console.WriteLine ( "Изменения зафиксированы" );
}
public void Rollback ()  {
Console.WriteLine ( "Изменения отменены" );
}
public class RollbackHelper : IDisposable {
public RollbackHelper ( Database db )  {
this.db = db;
-RollbackHelper () {
Dispose ( false );
}
public void Dispose ()  {
Dispose ( true );
}
public void Commit ()  {
db.Commit();
committed = true;
}
private void Dispose ( bool disposing )  {
// Если объект уже освобожден, то не делать ничего. Помните,
// что вызывать Dispose () несколько раз на одном объекте овершенно законно,
if ( !disposed )  {
disposed = true;
// Помните, что мы не хотим ничего делать с db,
// если попали сюда из финализатора, поскольку
// поле базы данных может быть уже финализированным!
if ( disposing )  {
if ( !committed )  {
db.Rollback();
}
} else {
Debug.Assert( false, "Сбой при вызове Dispose()" + " на RollbackHelper" ) ;
}
private Database db;
private bool disposed = false;
private bool committed = false;
}
public class EntryPoint {
static private void DoSomeWork()  {
using( RollbackHelper guard = new RollbackHelper(db)  )  {
// Здесь выполняем некоторую работу, которая может сгенерировать исключение.
// Удалите комментарий со следующей строки, чтобы сгенерировать исключение:
// nullPtr.GetType();
// Если добрались сюда, фиксируем,
guard.Commit();
}
}
static void Main()  {
db = new Database () ;
DoSomeWork();
}
static private Database db;
static private Object nullPtr = null;

Внутри метода DoSomeWork выполняется работа, которая может дать сбой с выдачей исключения. Если сгенерируется исключение, необходимо отменить все изменения, проведенные в объекте Database. Внутри блока using создается новый объект RollbackHelper, содержащий ссылку на объект Database.

Если поток управления достигает точки вызова Commit на ссылке guard, значит, все хорошо, если предположить, что метод Commit не сгенерирует исключений.

Даже если это произойдет, он должен кодироваться таким образом, чтобы Database оставалась в корректном состоянии. Однако если код внутри защищенного блока сгенерирует исключение, то метод Dispose на RollbackHelper аккуратно выполнит откат базы данных.

Независимо от того, что случилось, благодаря блоку using, метод Dispose будет вызван на экземпляре RollbackHelper. Если вы забудете применить блок using, то финализатор RollbackHelper не сможет ничего сделать, поскольку финализация объектов происходит в случайном порядке, и экземпляр Database, на который ссылается RollbackHelper, может быть финализирован до экземпляра RollbackHelper.

Чтобы помочь найти место, где допущена ошибка, можно закодировать утверждение (assertion) во вспомогательном объекте, как это было сделано выше. Поскольку применение всего шаблона зиждется на блоке using, для целей дальнейшего обсуждения предположим, что вы не забыли о нем.

Как только поток управления благополучно достигнет метода Dispose и попадет туда через явный вызов Dispose, а не через финализатор, он просто проверит флаг фиксации, и если он не установлен, то вызовет Rollback на экземпляре Database. Вот и все.

Получается почти столь же элегантно, как в решении на С++, за исключением того, что, нужно не забыть о применении ключевого слова using, чтобы все это работало. Если хотите посмотреть, что произойдет в случае генерации исключения, просто удалите комментарий со строки, где производится попытка обращения к нулевой ссылке внутри метода DoSomeWork.

Возможно, вы заметили, что случай, когда Rollback сгенерирует исключение, никак не учитывается.

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

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

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

Но что, если такое случится в С#? Вспомните, что блок using разворачивается в конструкцию try/finally. Когда исключение генерируется в блоке finally, который выполняется в результате предыдущего исключения, то предыдущее исключение просто теряется. Что еще хуже, так это то, что выполнявшийся перед этим блок finally не может быть завершен.

Поэтому с учетом того, что утеря информации об исключении — это всегда плохо и весьма затрудняет поиск причин проблемы, настоятельно рекомендуется никогда не генерировать исключения внутри блока finally.

Хотя об этом уже упоминалось ранее, но никогда не будет лишним повторить. Если исключение будет сгенерировано во время выполнения блока finally, то среда CLR не прервет приложение, но приложение, скорее всего, окажется в неопределенном состоянии.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100