Схема Бертрана обработки исключительных ситуаций
Схема обработки исключительных ситуаций, предложенная в языке C#, обладает одним существенным изъяном - ее можно применить некорректно. Она позволяет, в случае возникновения исключительной ситуации, уведомить о ее возникновении и спокойно продолжить работу, что в конечном счете приведет к неверным результатам. Из двух зол - прервать вычисление с уведомлением о невозможности продолжения работы или закончить вычисления с ошибочным результатом вычисления - следует выбирать первое. Некорректно примененная схема C# приведет к ошибочным результатам. Приведу несколько примеров. Представьте, оформляется заказ на отдых где-нибудь на Канарах. В ходе оформления возникает исключительная ситуация - нет свободных мест в гостинице - обработчик исключения посылает уведомление с принесением извинений, но оформление заказа продолжается. Вероятно, предпочтительнее отказаться от отдыха на Канарах и выбрать другое место, чем оказаться без крыши над головой, ночуя на берегу океана. Эта ситуация не является критически важной. А что, если в процессе подготовки операции выясняется, что проведение ее в данном случае опасно? Никакие извинения не могут избавить от вреда, нанесенного операцией. Операция должна быть отменена.
Бертран Мейер в книге [1], в которой все механизмы, используемые в объектной технологии, тщательно обосновываются, предложил следующую схему обработки исключительных ситуаций. В основе ее лежит подход к проектированию программной системы на принципах Проектирования по Контракту. Модули программной системы, вызывающие друг друга, заключают между собой контракты. Вызывающий модуль обязан обеспечить истинность предусловия, необходимого для корректной работы вызванного модуля. Вызванный модуль обязан гарантировать истинность постусловия по завершении своей работы. Если в вызванном модуле возникает исключительная ситуация, то это означает, что он не может выполнить свою часть контракта. Что должен делать обработчик исключительной ситуации? У него только две возможности - Retry и Rescue. Первая (Retry) - попытаться внести некоторые коррективы и вернуть управление охраняемому модулю, который может предпринять очередную попытку выполнить свой контракт. Модуль может, например в следующей попытке запустить другой алгоритм, использовать другой файл, другие данные. Если все закончится успешно и работа модуля будет соответствовать его постусловию, то появление исключительной ситуации можно рассматривать как временные трудности, успешно преодоленные. Если же ситуация возникает вновь и вновь, тогда обработчик события применяет вторую стратегию (Rescue), выбрасывая исключение и передавая управление вызывающему модулю, который и должен теперь попытаться исправить ситуацию. Важная тонкость в схеме, предложенной Бертраном, состоит в том, что исключение, выбрасываемое обработчиком, следует рассматривать не как панику, не как бегство, а как отход на заранее подготовленные позиции. Обработчик исключения должен позаботиться о восстановлении состояния, предшествующего вызову модуля, который привел к исключительной ситуации, и это гарантирует нахождение всей системы в корректном состоянии.
Схема Бертрана является схемой с возобновлением, и она наиболее точно описывает разумную стратегию обработки исключительных ситуаций. Не следует думать, что эта схема не может быть реализована на C#, просто она требует понимания сути и определенной структурной организации модуля. Приведу возможную реализацию такой схемы на C#:
public void Pattern() { do { try { bool Danger = false; Success = true; MakeJob(); Danger = CheckDanger(); if (Danger) throw (new MyException()); MakeLastJob(); } catch (MyException me) { if(count > maxcount) throw(new MyException("Три попытки были безуспешны")); Success = false; count++; //корректировка ситуации Console.WriteLine("Попытка исправить ситуацию!"); level +=1; } }while (!Success); }
Приведу краткие комментарии к этой процедуре, которую можно рассматривать как некоторый образец организации обработки исключительной ситуации:
- Конструкция try-catch блоков помещается в цикл do-while(!Success), завершаемый в случае успешной работы охраняемого блока, за чем следит булева переменная Success.
- В данном образце предполагается, что в теле охраняемого блока анализируется возможность возникновения исключительной ситуации и, в случае обнаружения опасности, выбрасывается собственное исключение, класс которого задан программно. В соответствии с этим тело try-блока содержит вызов метода MakeJob, выполняющего некоторую часть работы, после чего вызывается метод CheckDanger, выясняющий, не возникла ли опасность нарушения спецификации и может ли работа быть продолжена. Если все нормально, то выполняется метод MakeLastJob, выполняющий заключительную часть работы. Управление вычислением достигает конца try-блока, он успешно завершается и, поскольку остается истинной переменная Success, значение true которой установлено в начале try-блока, то цикл while, окаймляющий охраняемый блок и его обработчиков исключений, также успешно завершается.
- Если в методе CheckDanger выясняется, что нормальное продолжение вычислений невозможно, то выбрасывается исключение класса MyException. Оно перехватывается обработчиком исключения, стоящим за try-блоком, поскольку класс MyException указан как класс формального аргумента.
- Для простоты приведен только один catch-блок. В общем случае их может быть несколько, но все они строятся по единому образцу. Предполагается, что обработчик исключения может сделать несколько попыток исправить ситуацию, после чего повторно выполняется охраняемый блок. Если же число попыток, за которым следит переменная count, превосходит максимально допустимое, то обработчик выбрасывает новое исключение, задавая дополнительную информацию и передавая тем самым обработку ошибки на следующий уровень - вызываемой программе.
- Когда число попыток еще не исчерпано, обработчик исключения переменной Success дает значение false, гарантирующее повтор выполнения try-блока, увеличивает счетчик числа попыток и пытается исправить ситуацию.
- Как видите, эта схема реализует два корректных исхода обработки исключительной ситуации - Retry и Rescue - повтору с надеждой выполнить обязательства и передачи управления вызывающей программе, чтобы она предприняла попытки исправления ситуации, когда вызванная программа не смогла с этим справиться.
Доведем этот образец до реально работающего кода, где угроза исключения зависит от значения генерируемого случайного числа, а обработчик исключения может изменять границы интервала, повышая вероятность успеха.
Определим первым делом собственный класс исключений:
public class MyException :Exception { public MyException() {} public MyException (string message) : base(message) {} public MyException (string message, Exception e) : base(message, e) {} }
Минимум того, что нужно сделать, определяя свои исключения, - это задать три конструктора класса, вызывающие соответствующие конструкторы базового класса Exception.
В классе Excepts, методом которого является наш образец Pattern, определим следующие поля класса:
Random rnd = new Random(); int level = -10; bool Success; //true - нормальное завершение int count =1; // число попыток выполнения const int maxcount =3;
Определим теперь методы, вызываемые в теле охраняемого блока:
void MakeJob() { Console.WriteLine("Подготовительные работы завершены"); } bool CheckDanger() { //проверка качества и возможности продолжения работ int low = rnd.Next(level,10); if ( low > 6) return(false); return(true); } void MakeLastJob() { Console.WriteLine("Все работы завершены успешно"); }
В классе Testing зададим метод, вызывающий метод Pattern:
public void TestPattern() { Excepts ex1 = new Excepts(); try { ex1.Pattern(); } catch (Exception e) { Console.WriteLine("исключительная ситуация при вызове Pattern"); Console.WriteLine(e.ToString()); } }
Обратите внимание, что вызов метода Pattern находится внутри охраняемого блока. Поэтому, когда Pattern не справится с обработкой исключительной ситуации, ее обработку возьмет на себя универсальный обработчик, стоящий за try-блоком.
На рис. 23.6 показаны три варианта запуска метода TestPattern. В одном из них исключительной ситуации при вызове метода Pattern вообще не возникало, в другом - ситуация возникала, но коррекция обработчика исключения помогла и при повторе выполнения охраняемого блока в Pattern все прошло нормально. В третьем варианте метод Pattern не смог справиться с исключительной ситуацией, и она обрабатывалась в catch-блоке метода TestPattern.
Рис. 23.6. Обработка исключительных ситуаций. Три случая