例外をスローするコンストラクターでエラーを記録する必要がありますか?


15

私は数か月間アプリケーションを構築していましたが、次のようなパターンが明らかになりました。

logger.error(ERROR_MSG);
throw new Exception(ERROR_MSG);

または、キャッチする場合:

try { 
    // ...block that can throw something
} catch (Exception e) {
    logger.error(ERROR_MSG, e);
    throw new MyException(ERROR_MSG, e);
}

したがって、例外をスローまたはキャッチするたびに、ログに記録します。実際、それはアプリケーションで行ったほとんどすべてのロギングでした(アプリケーションの初期化のための何かに加えて)。

それで、プログラマーとして、私は繰り返しを避けます。そこで、ロガー呼び出しを例外構築に移動することにしました。そのため、例外を構築するたびに、ログが記録されます。確かに、私のために例外をスローするExceptionHelperを作成することもできますが、それはコードの解釈を難しくし、さらに悪いことに、コンパイラはそれをうまく処理できず、そのメンバーへの呼び出しがすぐに投げます。

それで、これはアンチパターンですか?もしそうなら、なぜですか?


例外をシリアライズおよびデシリアライズするとどうなりますか?エラーを記録しますか?
黙示録

回答:


18

アンチパターンとして適格かどうかはわかりませんが、IMOは悪い考えです。ロギングと例外を絡めるのは不必要な結合です。

特定の例外のすべてのインスタンスを常にログに記録する必要はありません(入力検証中に発生する可能性があり、そのログは膨大で興味のないものになる可能性があります)。

第二に、異なるログレベルでエラーの異なる発生をログに記録することを決定する場合があります。その時点で、例外を構築するときにそれを指定する必要があります。

最後に、別の例外のロギング中に例外が発生した場合はどうなりますか?ログに記録しますか?乱雑になります...

基本的に次の選択肢があります。

  • 例で示したように、キャッチ、ログ、および(再)スロー
  • ExceptionHelperクラスを作成して両方を実行しますが、ヘルパーにはコードの匂いがあります。これもお勧めしません。
  • キャッチオール例外処理をより高いレベルに移動する
  • ロギングや例外処理などの横断的関心事に対するより洗練されたソリューションのためにAOPを検討してください(ただし、catchブロックにこれらの2行を置くよりもはるかに複雑です;))

「ボリュームがあり面白くない」場合は+1。AOPとは
Tulainsコルドバ

@ user61852アスペクト指向プログラミング(リンクを追加しました)。この質問Javaで一例を示し、W / R / T AOPとログを:stackoverflow.com/questions/15746676/logging-with-aop-in-spring
Dan1701

11

だから、プログラマとして、私は繰り返しを避けます[...]

ここで、その概念が"don't repeat yourself"あまりにも真剣に考えられて、それが匂いになるところに危険がある。


2
今、私はすべてが良いときに正しい答えを選び、お互いの上に築くのは一体どういうことでしょうか?私がそれに熱狂的なアプローチをとった場合、DRYがどのように問題になるかについての優れた説明。
ブルーノブラント

1
それはDRYで本当に良い刺しです、私は私がDRY中毒であることを告白しなければなりません。この5行のコードをDRYのために別の場所に移動することを考えるとき、2回検討します。
SZT

@SZaman私はもともと非常に似ていました。明るい面では、たとえば、コピーアンドペーストを使用して500行の関数を記述し、リファクタリングについても考えていない人よりも、冗長性を打ち消す側に偏りすぎている人の方がはるかに多くの希望があると思います。IMOを覚えておくべき主なことは、いくつかの小さな重複を打ち消すたびに、コードを分散化し、依存関係を別の場所にリダイレクトすることです。それは良いことでも悪いことでもあります。それは...あなたの変化挙動が、動作はまた、あなたを噛まないように始めることができると共有する中央制御することができます

@SZaman「この機能のみがこれを必要とし、この中央機能を使用する他の人は必要ありません」のような変更を行いたい場合。とにかく、それは私が見るようにバランスのとれた行為です-完全に行うのは難しいことです!ただし、コードを少し独立させると、コードの独立性と分離性が向上する場合があります。そして、コードの一部をテストして、あちこちでいくつかの基本的なロジックを複製している場合でも、それが本当にうまく機能する場合、それを変更する理由はほとんどないかもしれません。一方、外部の物事に大きく依存するものは、変更しなければならない外部の理由をより多く見つけます。

6

@ Dan1701をエコーするには

これは、懸念事項の分離に関するものです-ロギングを例外に移動すると、例外とロギングが緊密に結合され、ロギングの例外に追加の責任が追加され、それが例外クラスの依存関係を作成する可能性があります必要ありません。

メンテナンスの観点から、(少なくとも例のようなコンテキストでは)例外がメンテナーから記録されているという事実を隠していると主張することができます(少なくとも例のようなコンテキストで)例外ハンドラーの場所から例外のコンストラクターまで)。これはおそらく意図したものではありませ

最後に、例外を作成/発生した時点で、まったく同じ方法で常にログに記録することを前提としています。これはおそらくそうではありません。すぐに非常に不快になるロギングおよび非ロギング例外がある場合を除きます。

この場合、「SRP」は「DRY」に勝ると思います。


1
[...]"SRP" trumps "DRY"-この引用はほぼ完璧にまとめていると思います。

@Ikeが言ったように...これは私が探していた一種の理論的根拠です。
ブルーノブラント

ロギングのコンテキストを変更すると、例外クラスがオリジンまたはログエントリであるかのようにログに記録されることを示すために+1。
Tulainsコルドバ

2

エラーは、処理できない例外をログに記録するため、ログが適切に処理されているかどうかを知ることができません。
エラーの一部を処理できる、または処理する必要があるケースはほとんどありません。ログにはログが含まれる場合がありますが、それでも呼び出し元にエラーを通知する必要があります。例としては、最低レベルの読み取りを行う場合の修正不可能な読み取りエラーなどがありますが、一般に、セキュリティと使いやすさのために、発信者に伝達される情報が厳しくフィルタリングされるという共通点があります。

あなたがあなたの場合にできる唯一のことは、何らかの理由でやらなければならないことです。実装がスローする例外を呼び出し元が期待する例外に変換し、元のコンテキストを連鎖させ、他のものはそのままにしておきます。

要約すると、コードは例外の部分的な処理の権利と義務を放棄し、SRPに違反しています。
DRYは入りません。


1

catchブロックを使用してログに記録することを決定した(例外がまったく変更されていないことを意味する)ためだけに例外を再スローすることは、悪い考えです。

例外、例外メッセージ、およびその処理を使用する理由の1つは、何が悪かったのか、巧妙に書かれた例外を知って、バグの発見を大幅にスピードアップできるようにするためです。

また、例外を処理するとif、たとえばを持っているよりも多くのリソースが必要になるので、気が付いたという理由だけで例外をすべて処理することは避けてください。アプリケーションのパフォーマンスに影響を与えます。

ただし、エラーが発生したアプリケーション層をマークする手段として例外を使用することは良いアプローチです。

次の準擬似コードを検討してください。

interface ICache<T, U>
{
    T GetValueByKey(U key); // may throw an CacheException
}

class FileCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from FileCache::getvalueByKey. The File could not be opened. Key: " + key);
    }
}

class RedisCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: " + key);
    }
}

class CacheableInt
{
    ICache<int, int> cache;
    ILogger logger;

    public CacheableInt(ICache<int, int> cache, ILogger logger)
    {
        this.cache = cache;
        this.logger = logger;
    }

    public int GetNumber(int key) // may throw service exception
    {
        int result;

        try {
            result = this.cache.GetValueByKey(key);
        } catch (Exception e) {
            this.logger.Error(e);
            throw new ServiceException("CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: " + key);
        }

        return result;
    }
}

class CacheableIntService
{
    CacheableInt cacheableInt;
    ILogger logger;

    CacheableInt(CacheableInt cacheableInt, ILogger logger)
    {
        this.cacheableInt = cacheableInt;
        this.logger = logger;
    }

    int GetNumberAndReturnCode(int key)
    {
        int number;

        try {
            number = this.cacheableInt.GetNumber(key);
        } catch (Exception e) {
            this.logger.Error(e);
            return 500; // error code
        }

        return 200; // ok code
    }
}

誰かがコードを呼び出しGetNumberAndReturnCode500コードを受け取り、エラーを通知したと仮定しましょう。彼はサポートに電話し、ログファイルを開いてこれを確認します。

ERROR: 12:23:27 - Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: 28
ERROR: 12:23:27 - CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: 28

その後、開発者は、ソフトウェアのどの層がプロセスを中断させたかをすぐに把握し、問題を簡単に特定できます。この場合、Redisのタイムアウトが発生することはないため、重要です。

別のユーザーが同じメソッドを呼び出して500コードを受け取ることもありますが、ログには次のように表示されます。

INFO: 11:11:11- Could not retrieve object from RedisCache::getvalueByKey. Value does not exist for the key 28.
INFO: 11:11:11- CacheableInt::GetNumber failed, because the cache layer could not find any data for the key 28.

この場合、サポートはユーザーが存在しないIDの値を要求しているため、要求が無効であるとユーザーに単純に応答できます。


概要

例外を処理している場合は、必ず正しい方法で処理してください。また、例外には、アーキテクチャレイヤーに続く正しいデータ/メッセージが含まれていることを確認してください。メッセージは、発生する可能性のある問題を特定するのに役立ちます。


1

問題はより基本的なレベルにあると思います。エラーをログに記録し、同じ場所で例外としてスローします。それがアンチパターンです。これは、同じエラーがキャッチされた場合に何度もログに記録され、おそらく別の例外にラップされて再スローされることを意味します。

これの代わりに、例外が作成されたときではなく、キャッチされたときにエラーを記録することをお勧めします。(もちろん、このため、常にどこかでキャッチされることを確認する必要があります。)例外がキャッチされた場合、そのスタックトレースを再スローまたは原因として別の例外にラップしない限り、ログを記録します。とにかく、スタックトレースとラップされた例外のメッセージは、スタックトレースに「Caused by ...」として記録されます。また、キャッチャーは、たとえば、最初の失敗でエラーをログに記録せずに再試行するか、単に警告として処理するかなどを決定することもできます。


1

古いスレッドであることは知っていますが、同様の問題に遭遇し、同様の解決策を見つけたので、2セントを追加します。

私はSRP違反の議論を買いません。とにかく完全ではありません。次の2つのことを想定しましょう。1.実際に、例外が発生したときに(プログラムフローを再作成できるようにトレースレベルで)ログに記録します。これは、例外の処理とは関係ありません。2.そのためにAOPを使用できない、または使用しない-それが最善の方法であることに同意しますが、残念ながら、そのためのツールを提供しない言語にこだわっています。

私の見方では、例外をスローしたいクラスはログを認識している必要があるため、基本的に大規模なSRP違反の宣告を受けています。ロギングを例外クラスに移動すると、コードベースのすべてのクラスではなく例外のみに違反するため、実際にはSRP違反が大幅に減少します。


0

これはアンチパターンです。

私の意見では、例外のコンストラクターでロギング呼び出しを行うことは、次の例になります。欠陥:コンストラクターはReal Workです。

私は、コンストラクターが何らかの外部サービス呼び出しを行うことを決して期待しません(または望みません)。これは非常に望ましくない副作用であり、MiškoHeveryが指摘しているように、サブクラスとモックに不要な動作を継承させます。

このように、それはまた、最小限の驚きの原則に違反するでしょう。

他の人と一緒にアプリケーションを開発している場合、この副作用はおそらく他の人には明らかではありません。一人で仕事をしている場合でも、そのことを忘れて、自分自身を驚かせるかもしれません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.