Исключения в C++ — один из самых серьезных механизмов языка.
Предоставляет довольно мощные возможности для анализа и обработки ошибок.
Но работать с исключениями не всегда так удобно.
В этой статье я хочу поделиться решением, которое успешно используется в проекте, над которым я сейчас работаю.
Думаю, самые умные люди уже поняли, в чем моя идея.
Кому еще интересно, предлагаю разобраться в этом подробнее.
Проект, над которым я сейчас работаю, имеет плагинную архитектуру, со своими плагинами для работы с ресурсами, консолью и многим другим.
Не так давно мы начали писать модульные тесты для новых модулей, и первой задачей, с которой мы столкнулись, было максимально избавиться от использования других плагинов в коде.
Это привело к новому подходу к проектированию модулей, и исключения сыграли здесь важную роль.
У нас принято, что исключения не хранят никаких сообщений о произошедшей ошибке (так как для их поиска придется обращаться к ресурсам).
В лучшем случае через What() можно узнать файл и строку, откуда было выброшено исключение.
Каждый тип ошибки должен иметь свой тип исключения.
Класс исключений должен хранить только те значения, которые необходимы для анализа ошибки.
Наша система относительно большая, и при таком подходе каждый плагин имеет иерархию от 7 до 20 исключений.
Первой проблемой, с которой мы столкнулись, было дублирование кода.
В лучшем случае у каждого плагина есть три точки, из которых можно вызвать код, генерирующий исключения, и каждая такая точка обрастает большим перехватом, где для каждого типа исключения выводится свой текст ошибки или происходит какая-то другая обработка.
выполненный.
С добавлением новых исключений становится еще веселее; программист должен пропустить хотя бы одну точку перехвата исключения, и появится потенциальная ошибка.
Конечно, такие ситуации сразу покрываются юнит- или компонентными тестами и быстро выявляются, но, как говорится: «лучшее исправление ошибки — исключить возможность ее возникновения».
Идея в том, что исключения каждого модуля имеют свой базовый класс с методами работы механизма посетителя:
Также создается гостевой интерфейс, в котором объявляются методы для «посещения» всех типов исключений модуля: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
Список ловушек исчезает, как и дублирование кода.
Весь код обработки размещен в посетителях, которыми можно легко пользоваться из нескольких мест. Также при таком подходе вы можете быть уверены, что перехватите все исключения модуля, с которым работаете, и при этом правильно обработаете каждое из них.
При расширении списка исключений вы можете быть уверены, что разработчик не сможет пропустить добавление обработки в любом месте, так как, добавив ваш класс в базовый класс посетителя, все остальные посетители просто не будут компилироваться, пока каждый из них не будет компилироваться.
реализует обработку, необходимую для конкретного случая.
Когда использовать этот подход
- В проекте используется большая иерархия исключений или просто большое количество исключений, которые, возможно, стоит обобщить;
- существует более одной точки одинакового перехвата и обработки исключений.
Положительные стороны
- Позволяет писать более гибкие обработчики исключений;
- позволяет использовать один обработчик в нескольких местах;
- практически исключает возможность «забыть» добавить обработку новых исключений во всех местах, где это необходимо.
Отрицательные стороны
- Создание каждый раз дополнительных объектов (классов посетителей) для обработки исключений;
- Каждое исключение должно иметь свой уникальный тип исключения.
Послесловие
Этот подход не является чем-то новым, и я не пытаюсь открыть новый шаблон проектирования.В этой статье я постарался поделиться решением, которое показалось мне оптимальным для решения моих задач.
Надеюсь, этот опыт будет полезен и вам.
Теги: #c plus plus #исключения #шаблоны #шаблоны #visitor #C++
-
Рид, Томас
19 Oct, 24 -
Поддельное Устройство Ble На Nrf24L01
19 Oct, 24 -
Intel И Siemens Становятся Партнерами
19 Oct, 24 -
Последовательность Построения В Tfs
19 Oct, 24