Поиск

Обработка ошибок в правильном контексте

Один из основных принципов хорошего программирования — сильная связность (tight cohesion), когда речь идет о назначении методов. Сильно связанными называются методы, выполняющие единственную задачу. Главное достоинство сильной связности в том, что если некий метод выполняет единственное действие, он с большой степенью вероятности будет переносимым и сможет применяться в разных сценариях. Разумеется, такой метод гораздо проще отлаживать и сопровождать. Однако по отношению к обработке ошибок сильно связный код порождает одну главную проблему. Рассмотрим пример.

Класс Access Database применяется для генерации и работы с БД Microsoft Access. Допустим, у этого класса есть статический метод Generate-Database. Поскольку он используется для создания новой БД Access, ему предстоит выполнить несколько задач. Например, он должен создать физический файл БД и определенные таблицы (включая нужные вашему приложению строки и столбцы), а также определить все индексы и связи. Возможно, этот метод должен даже создать некоторых пользователей по умолчанию и разрешения.

Проблема со структурой программы такова: какой метод и как должен обрабатывать ошибку, возникшую в методе Createlndexesl Кажется очевидным, что обрабатывать ошибку должен метод, вызвавший Gene-rateDatabase, но как он это будет делать? Он не будет понимать, как обрабатывать ошибку, которая возникла на несколько уровней ниже. Как мы говорили, вызывающий метод находится не в том контексте, чтобы обрабатывать ошибку. Другими словами, поддающуюся интерпретации информацию об ошибке может создать только тот метод, в котором произошел сбой. В таком случае, если в классе Access Database используются коды возврата, каждый метод должен проверять каждую отдельную ошибку, которую каждый другой метод обязан возвращать. Очевидной проблемой является то, что вызывающему методу верхнего уровня потенциально придется обрабатывать безумное число кодов ошибок. Кроме того, это затрудняет сопровождение. Каждый раз, когда для любого метода будет добавляться новый код ошибки, нужно будет обновлять каждый экземпляр, в котором есть обращение к измененному методу, чтобы он обрабатывал новый код ошибки. Излишне говорить, что это недешевое решение в терминах совокупной стоимости владения (total cost of ownership, TCO).

Обработка исключений решает все эти проблемы, позволяя вызывающему методу реагировать на определенные типы исключений. В нашем примере, если класс AccessDatabaseException является производным от Exception, его можно использовать для любых типов ошибок, возникающих в любом из методов AccessDatabase. (Я расскажу о классе Exception и о создании собственных производных классов исключений в разделе "Класс System. Exception" .) В таком случае, если в методе Create Indexes произойдет сбой, он должен построить и передать исключение типа AccessDatabseException. Вызывающий метод уловит это исключение и сможет проанализировать объект Exception, чтобы разобраться, что именно произошло. Таким образом, вместо того чтобы обрабатывать каждый возможный код возврата, который возвращает GenerateDatbase и любой из вызываемых им методов, вызывающий метод уверен, что если сбой произойдет в любом из методов, то будет возвращена корректная информация об ошибке. Обработка исключений дает еще одно преимущество: поскольку информация об ошибке содержится в классе, при добавлении новых кодов ошибок вызывающий метод изменять не нужно. А разве расширяемость — возможность создания чего-либо и дальнейшее добавление без изменения или разрушения существующего кода — не одно из основных качеств ООП, с которого все начинается? Так что концепция улавливания и обработки ошибок в правильном контексте — одно из самых значительных преимуществ обработки исключений.