例外階層を処理するための訪問者パターン

C ++の例外は、最も深刻な言語メカニズムの1つです。 分析とエラー処理に十分な強力な機能を提供します。 しかし、例外を扱うことは必ずしもそれほど便利ではありません。



この記事では、現在作業中のプロジェクトに正常に適用されるソリューションを共有したいと思います。 最も独創的な人は、私の考えが何であるかをすでに理解していると思います。 他の誰に興味があるのか​​、もっと詳しく理解することをお勧めします。



現在作業しているプロジェクトには、リソース、コンソールなどを操作するための独自のプラグインを備えたプラグインアーキテクチャがあります。 少し前に、新しいモジュールの単体テストの作成を開始しました。最初のタスクは、コード内の他のプラグインの使用を可能な限り排除することでした。 これにより、モジュールの設計に対する新しいアプローチがもたらされ、ここでの例外は主な役割の1つとなりました。



例外は、発生したエラーに関するメッセージを保存しないことを決定しました(それらはリソースに移動する必要があるため)。 最良の場合、what()を介して、例外がスローされたファイルと行を見つけることができます。 各タイプのエラーには、独自のタイプの例外が必要です。 例外クラスには、エラー分析に必要な値のみを格納する必要があります。 私たちのシステムは比較的大きく、このアプローチでは、各プラグインに7〜20の例外の階層があります。



最初に遭遇した問題は、コードの重複です。 最良の場合、各プラグインには、例外をスローするコードを呼び出すことができる3つのポイントがあり、各ポイントには大きなキャッチリストがあり、各タイプの例外に対して独自のエラーテキストが表示されるか、他の処理が実行されます。 新しい例外が追加されると、さらに楽しくなり、プログラマーは例外をキャッチする点を少なくとも1つスキップする必要があり、潜在的なバグが発生します。 もちろん、このような状況はユニットまたはコンポーネントのテストですぐにカバーされ、すぐに検出されますが、「エラーの最善の修正はその発生の可能性を排除することです」と言います。



アイデアは、各モジュールの例外には、ビジターメカニズムが機能するメソッドを持つ独自の基本クラスがあるということです。

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; }
      
      





例外::訪問者::メッセンジャーの訪問者は次のようになります。

 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
      
      





キャッチリストが消え、コードも重複します。 すべての処理コードは訪問者に送信され、複数の場所から簡単に使用できます。 また、このアプローチを使用すると、作業中のモジュールのすべての例外を確実にキャッチし、同時にそれらの例外をそれぞれ正しく処理することができます。 例外のリストを展開するとき、ビジターの基本クラスに自分のクラスを追加するため、開発者が任意の場所への処理の追加をスキップできないことを確認できます。他のすべてのビジターは、それぞれが特定のケースに必要な処理を実装するまでコンパイルされません。



このアプローチを使用する場合




プラス側




マイナス面




あとがき


このアプローチは新しいものではなく、新しいデザインパターンを発見しようとはしていません。 この記事では、問題を解決するのに最適と思われる解決策を共有しようとしました。 この経験があなたのお役に立てば幸いです。




All Articles