Исключения в 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'ей пропадает, дублирование кода тоже. Весь код по обработки выносится в визиторы, которые легко можно использовать из нескольких мест. Так же при таком подходе можно быть уверенным, что ты поймаешь все исключения модуля с которым работаешь и при этом обработаешь каждый из них корректно. При расширении списка исключений можно быть уверенным, что разработчик не сможет пропустить добавить обработку в какое либо место, так как добавив свой класс в базовый класс визитора, все остальные визиторы просто не скомпилируются, пока в каждом из них не реализует необходимую для конкретного случая обработку.
Когда стоит использовать такой подход
- В проекте используется большая иерархия исключений или просто большое количество исключений которые возможно стоит обобщить;
- точек одинаковой поимки и обработки исключений больше одной.
Положительные стороны
- Позволяет писать более гибкие обработчики исключений;
- позволяет использовать один обработчик в нескольких местах;
- практически исключает возможность «забыть» добавить обработку новых исключений во всех местах где это необходимо.
Отрицательные стороны
- Создание каждый раз дополнительных объектов( классы визиторы ) для обработки исключений;
- для каждой исключительной ситуации должен быть свой уникальный тип исключения.
Послесловие
Данный подход не является чем-то новым, и я не стремлюсь открыть новый паттерн проектирования. В этой статье я попытался поделиться решением, которое как мне показалось, оптимальное для решения моих задач. Надеюсь данный опыт пригодится и вам.