Исключения в C++ являются одним из самых серьезных механизмов языка. Предоставляя достаточно мощные возможности для анализа и обработки ошибок. Но работа с исключениями не всегда бывает такой уж удобной.
В этой статье я хочу поделиться решением, которое успешно применяется в проекте с которым я сейчас работаю. Думаю самые догадливые уже поняли в чем заключается моя идея. Кому еще интересно предлагаю разобраться подробнее.
Проект с которым я сейчас работаю имеет плагиную архитектуру, со своими плагинами для работы с ресурсами, консолью, и еще много чем. Не так давно мы начали писать unit тесты для новых модулей, и первая же задача которая встала перед нами — это максимально избавиться в коде от использования других плагинов. Это вылилось в новый подход к проектированию модулей, и исключения здесь заняли одну из главных ролей.
У нас принято, что исключения не хранят в себе никаких сообщений об произошедшей ошибке( так как за ними придется лезть в ресурсы ). В лучшем случае через what() можно узнать файл и строку от куда исключение было брошено. Для каждого вида ошибки должен соответствовать свой тип исключения. Класс исключения должен хранить только значения необходимые для анализа ошибки. Система у нас относительно большая, и с таким подходом у каждого плагина существуют иерархии из 7 — 20 исключений.
Первая же проблемы с которой мы столкнулись — это дупликация кода. В лучшем случае в каждом плагине есть три точки от куда может быть вызван код который генерирует исключения, и каждая такая точка обрастает большим списком catch'ей, где для каждого вида исключения выводится свой текст ошибки либо выполняется какая либо другая обработка. С добавлением новых исключений становится еще веселее, программисту стоит пропустить хотя бы одну точку поимки исключений, и появляется потенциальный баг. Конечно, такие ситуации сразу же покрываются unit или компонентными тестами и обнаруживаются быстро, но как говорится: «лучшее исправление ошибки — это исключить возможность ее появления».
Идея в том, чтобы исключения каждого модуля имели свой базовый класс с методами для работы механизма визиторов:
Также создается интерфейс визитора в котором декларируются методы для «посещения» всех видов исключений модуля:
Примерно так будет выглядеть реализация исключений:
Вместо поимки всех видов исключений, ловится только базовый тип, и для обработки такого исключения используется визитор с необходимым назначением:
Визитор Exceptions::Visitors::Messenger будет выглядеть примерно так:
Список catch'ей пропадает, дублирование кода тоже. Весь код по обработки выносится в визиторы, которые легко можно использовать из нескольких мест. Так же при таком подходе можно быть уверенным, что ты поймаешь все исключения модуля с которым работаешь и при этом обработаешь каждый из них корректно. При расширении списка исключений можно быть уверенным, что разработчик не сможет пропустить добавить обработку в какое либо место, так как добавив свой класс в базовый класс визитора, все остальные визиторы просто не скомпилируются, пока в каждом из них не реализует необходимую для конкретного случая обработку.
Данный подход не является чем-то новым, и я не стремлюсь открыть новый паттерн проектирования. В этой статье я попытался поделиться решением, которое как мне показалось, оптимальное для решения моих задач. Надеюсь данный опыт пригодится и вам.
В этой статье я хочу поделиться решением, которое успешно применяется в проекте с которым я сейчас работаю. Думаю самые догадливые уже поняли в чем заключается моя идея. Кому еще интересно предлагаю разобраться подробнее.
Проект с которым я сейчас работаю имеет плагиную архитектуру, со своими плагинами для работы с ресурсами, консолью, и еще много чем. Не так давно мы начали писать unit тесты для новых модулей, и первая же задача которая встала перед нами — это максимально избавиться в коде от использования других плагинов. Это вылилось в новый подход к проектированию модулей, и исключения здесь заняли одну из главных ролей.
У нас принято, что исключения не хранят в себе никаких сообщений об произошедшей ошибке( так как за ними придется лезть в ресурсы ). В лучшем случае через what() можно узнать файл и строку от куда исключение было брошено. Для каждого вида ошибки должен соответствовать свой тип исключения. Класс исключения должен хранить только значения необходимые для анализа ошибки. Система у нас относительно большая, и с таким подходом у каждого плагина существуют иерархии из 7 — 20 исключений.
Первая же проблемы с которой мы столкнулись — это дупликация кода. В лучшем случае в каждом плагине есть три точки от куда может быть вызван код который генерирует исключения, и каждая такая точка обрастает большим списком catch'ей, где для каждого вида исключения выводится свой текст ошибки либо выполняется какая либо другая обработка. С добавлением новых исключений становится еще веселее, программисту стоит пропустить хотя бы одну точку поимки исключений, и появляется потенциальный баг. Конечно, такие ситуации сразу же покрываются unit или компонентными тестами и обнаруживаются быстро, но как говорится: «лучшее исправление ошибки — это исключить возможность ее появления».
Идея в том, чтобы исключения каждого модуля имели свой базовый класс с методами для работы механизма визиторов:
namespace Exceptions { struct IBase : public std::exception { virtual void accept( IVisitor & _visitor ) const throw()= 0; }; } // namespace Exceptions
Также создается интерфейс визитора в котором декларируются методы для «посещения» всех видов исключений модуля:
namespace Exceptions { struct IVisitor { virtual ~IVisitor() {} virtual void visit( CannotOpenFile const & _exception ) = 0; virtual void visit( NotHaveSpace const & _exception ) = 0; }; } // namespace Exceptions
Примерно так будет выглядеть реализация исключений:
namespace Exceptions { class CannotOpenFile : public IBase { public: CannotOpenFile( std::string const & _filePath ) throw () : m_path( _filePath ) { } std::string const & getPath() const throw() { return m_path; } /*virtual*/ void accept( IVisitor & _visitor ) const throw() { _visitor.visit( *this ); } private: const std::string m_path; }; class NotHaveSpace : public IBase { public: /*virtual*/ void accept( IVisitor & _visitor ) const throw() { _visitor.visit( *this ); } }; } // namespace Exceptions
Вместо поимки всех видов исключений, ловится только базовый тип, и для обработки такого исключения используется визитор с необходимым назначением:
int main( int /*_argc*/, char * /*_argv[]*/ ) { int result = EXIT_SUCCESS; try { MyFile file; file.open( "file.my" ); } catch( Exceptions::IBase const & _exception ) { Exceptions::Visitors::Messenger visitor( std::cerr ); _exception.accept( visitor ); result = EXIT_FAILURE; } return result; }
Визитор Exceptions::Visitors::Messenger будет выглядеть примерно так:
namespace Exceptions { namespace Visitors { class Messenger : public IVisitor { public: Messenger( std::ostream & _outputStream ) : m_outputStream( outputStream ) {} /*virtual*/ void visit( CannotOpenFile const & _exception ) { m_outputStream << "Cannot open file: " << _exception.getPath() << std::endl; } /*virtual*/ void visit( NotHaveSpace const & /*_exception*/ ) { m_outputStream << "Not have space for saving file" << std::endl; } private: std::ostream & m_outputStream; }; } // namespace Visitors } // namespace Exceptions
Список catch'ей пропадает, дублирование кода тоже. Весь код по обработки выносится в визиторы, которые легко можно использовать из нескольких мест. Так же при таком подходе можно быть уверенным, что ты поймаешь все исключения модуля с которым работаешь и при этом обработаешь каждый из них корректно. При расширении списка исключений можно быть уверенным, что разработчик не сможет пропустить добавить обработку в какое либо место, так как добавив свой класс в базовый класс визитора, все остальные визиторы просто не скомпилируются, пока в каждом из них не реализует необходимую для конкретного случая обработку.
Когда стоит использовать такой подход
- В проекте используется большая иерархия исключений или просто большое количество исключений которые возможно стоит обобщить;
- точек одинаковой поимки и обработки исключений больше одной.
Положительные стороны
- Позволяет писать более гибкие обработчики исключений;
- позволяет использовать один обработчик в нескольких местах;
- практически исключает возможность «забыть» добавить обработку новых исключений во всех местах где это необходимо.
Отрицательные стороны
- Создание каждый раз дополнительных объектов( классы визиторы ) для обработки исключений;
- для каждой исключительной ситуации должен быть свой уникальный тип исключения.
Послесловие
Данный подход не является чем-то новым, и я не стремлюсь открыть новый паттерн проектирования. В этой статье я попытался поделиться решением, которое как мне показалось, оптимальное для решения моих задач. Надеюсь данный опыт пригодится и вам.
