📜 ⬆️ ⬇️

Visitor pattern for handling exception hierarchy

Exceptions in C ++ are one of the most serious mechanisms of a language. Providing powerful enough features for analyzing and handling errors. But working with exceptions is not always that convenient.

In this article I want to share the solution that is successfully used in the project with which I am currently working. I think the most shrewd ones have already understood what my idea is. To whom it is still interesting, I propose to sort out more.

The project with which I am working now has a plugin architecture, with its own plugins for working with resources, the console, and many more things. Not so long ago, we started writing unit tests for new modules, and the very first task we faced was to get rid of the use of other plug-ins in code. This resulted in a new approach to the design of modules, and the exceptions here occupied one of the main roles.
')
We have accepted that exceptions do not store any messages about an error that has occurred (since they will have to be crawled into resources). At best, through what () you can get the file and the line from where the exception was thrown. For each type of error, its own type of exception must correspond. The exception class must store only the values ​​required for error analysis. Our system is relatively large, and with this approach, each plug-in has a hierarchy of 7 to 20 exceptions.

The first problem we encountered is the duplication of the code. In the best case, each plug-in has three points from where the code that generates exceptions can be called, and each such point has a large catch list, where each type of exception displays its own error text or some other processing is performed. With the addition of new exceptions it becomes even more fun, the programmer should skip at least one point to catch exceptions, and a potential bug appears. Of course, such situations are immediately covered by the unit or component tests and are detected quickly, but as they say: “the best correction of an error is to eliminate the possibility of its occurrence”.

The idea is that the exceptions of each module should have its own base class with methods for the work of the visitors mechanism:
namespace Exceptions { struct IBase : public std::exception { virtual void accept( IVisitor & _visitor ) const throw()= 0; }; } // namespace Exceptions 

Also, a visitor interface is created in which the methods are declared for “visiting” all types of module exceptions:
 namespace Exceptions { struct IVisitor { virtual ~IVisitor() {} virtual void visit( CannotOpenFile const & _exception ) = 0; virtual void visit( NotHaveSpace const & _exception ) = 0; }; } // namespace Exceptions 

An implementation of exceptions would look something like this:
 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 

Instead of catching all types of exceptions, only the base type is caught, and to handle such an exception, a visitor with the necessary purpose is used:
 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; } 

The Exceptions :: Visitors :: Messenger visitor will look something like this:
 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 

The catch list disappears, duplication of code, too. All processing code is submitted to visitors, which can easily be used from several places. Also with this approach, you can be sure that you will catch all the exceptions of the module with which you work and at the same time process each of them correctly. When expanding the list of exceptions, you can be sure that the developer will not be able to skip adding processing to any place, since by adding your class to the base class of the visitor, all other visitors simply do not compile until each of them implements the necessary processing for a particular case.

When should you use this approach


Positive sides


Negative sides


Afterword

This approach is not something new, and I do not want to open a new design pattern. In this article I tried to share a solution that seemed to me optimal for solving my problems. I hope this experience will be useful to you.

Source: https://habr.com/ru/post/113606/


All Articles