Как стать автором
Обновить

Паттерн Visitor для обработки иерархии исключений

Время на прочтение4 мин
Количество просмотров4K
Исключения в C++ являются одним из самых серьезных механизмов языка. Предоставляя достаточно мощные возможности для анализа и обработки ошибок. Но работа с исключениями не всегда бывает такой уж удобной.

В этой статье я хочу поделиться решением, которое успешно применяется в проекте с которым я сейчас работаю. Думаю самые догадливые уже поняли в чем заключается моя идея. Кому еще интересно предлагаю разобраться подробнее.

Проект с которым я сейчас работаю имеет плагиную архитектуру, со своими плагинами для работы с ресурсами, консолью, и еще много чем. Не так давно мы начали писать 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'ей пропадает, дублирование кода тоже. Весь код по обработки выносится в визиторы, которые легко можно использовать из нескольких мест. Так же при таком подходе можно быть уверенным, что ты поймаешь все исключения модуля с которым работаешь и при этом обработаешь каждый из них корректно. При расширении списка исключений можно быть уверенным, что разработчик не сможет пропустить добавить обработку в какое либо место, так как добавив свой класс в базовый класс визитора, все остальные визиторы просто не скомпилируются, пока в каждом из них не реализует необходимую для конкретного случая обработку.

Когда стоит использовать такой подход

  • В проекте используется большая иерархия исключений или просто большое количество исключений которые возможно стоит обобщить;
  • точек одинаковой поимки и обработки исключений больше одной.

Положительные стороны

  • Позволяет писать более гибкие обработчики исключений;
  • позволяет использовать один обработчик в нескольких местах;
  • практически исключает возможность «забыть» добавить обработку новых исключений во всех местах где это необходимо.

Отрицательные стороны

  • Создание каждый раз дополнительных объектов( классы визиторы ) для обработки исключений;
  • для каждой исключительной ситуации должен быть свой уникальный тип исключения.

Послесловие

Данный подход не является чем-то новым, и я не стремлюсь открыть новый паттерн проектирования. В этой статье я попытался поделиться решением, которое как мне показалось, оптимальное для решения моих задач. Надеюсь данный опыт пригодится и вам.
Теги:
Хабы:
Всего голосов 31: ↑29 и ↓2+27
Комментарии44

Публикации