私は数か月間アプリケーションを作成していましたが、出現したパターンに気づきました。
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を作成することもできますが、その場合、コードの解釈が難しくなり、さらに悪いことに、コンパイラーはそれをうまく処理できず、そのメンバーへの呼び出しがすぐに投げます。
それで、これはアンチパターンですか?もしそうなら、なぜですか?
アンチパターンとして適格かどうかはわかりませんが、IMOは悪い考えです。例外とロギングを結びつけるのは不必要な結合です。
特定の例外のすべてのインスタンスを常にログに記録したくない場合があります(入力の検証中に発生する可能性があり、そのログ記録は膨大であり、興味深いものではない場合があります)。
第2に、異なるログレベルでエラーの異なる発生をログに記録することを決定する場合があります。その時点で、例外を構築するときに、ロギング動作で例外の作成を混乱させることを指定する必要があります。
最後に、別の例外のロギング中に例外が発生した場合はどうなりますか?記録しますか?汚くなります...
基本的に次の選択肢があります。
だから、プログラマーとして、私は繰り返しを避ける[...]
"don't repeat yourself"
の概念を少し真剣に考えすぎると、危険が伴います。においになります。
@ Dan1701をエコーするには
これは懸念事項の分離に関するものです。ロギングを例外に移動すると、例外とロギングの間に密接な結合が作成されます。また、ロギングの例外に追加の責任が追加され、その結果、例外クラスの依存関係が作成される可能性があります必要ありません。
メンテナンスの観点からは、(少なくとも例のようなコンテキストでは)例外がメンテナからログに記録されているという事実を隠している(私はそうする)ことができ、コンテキストも変更します(例外ハンドラの場所から例外のコンストラクタまで)おそらくこれはnotが意図したものです。
最後に、例外が作成/発生された時点で、まったく同じ方法で例外を常にログに記録したいという想定を立てていますが、これはおそらく当てはまりません。非常に迅速に非常に不快になるロギング例外と非ロギング例外がある場合を除きます。
したがって、この場合、「SRP」は「DRY」よりも優先されます。
あなたのエラーは、それを処理できない例外をログに記録しているため、ログが適切に処理されているかどうかを知ることができません。
ログの記録など、エラーの一部を処理できるか、または処理する必要があるケースはほとんどありませんが、まだは呼び出し元にエラーを通知する必要があります。例としては、最低レベルの読み取りを行う場合の修正不可能な読み取りエラーなどがありますが、一般に、セキュリティと使いやすさのために、呼び出し元に送信される情報が厳しくフィルター処理されるという共通点があります。
あなたのケースであなたができる唯一のこと、そして何らかの理由でやらなければならないことは、実装がスローする例外を呼び出し側が期待するものに変換し、元のコンテキストを連鎖させ、他のものはそのままにしておきます。
要約すると、あなたのコードは例外の部分的な処理の権利と義務を誇張しているため、SRPに違反しています。
DRYは含まれていません。
私はそれが古いスレッドであることを知っていますが、同じような問題に遭遇し、同じような解決策を見つけたので、2セントを追加します。
私はSRP違反の議論を買いません。とにかく完全ではない。次の2つを想定してみましょう。1.実際に、例外が発生したときに(プログラムフローを再作成できるようにトレースレベルで)ログに記録します。これは例外の処理とは関係ありません。 2. AOPを使用できない、または使用しない-これが最善の方法であることに同意しますが、残念ながら、そのためのツールを提供していない言語に悩まされています。
私が見るところ、例外をスローする必要があるクラスはログを認識している必要があるため、基本的に大規模なSRP違反の判決を受けます。ロギングを例外クラスに移動すると、実際にはSRP違反が大幅に削減されます。これは、例外のみが違反し、コードベースのすべてのクラスではないためです。
問題はより基本的なレベルにあると思います。エラーをログに記録し、同じ場所で例外としてスローします。それがアンチパターンです。つまり、同じエラーがキャッチされ、別の例外にラップされて再度スローされた場合、何度もログに記録されます。
これの代わりに、例外が作成されたときではなく、キャッチされたときにエラーをログに記録することをお勧めします。 (もちろん、このため、常にどこかでキャッチされることを確認する必要があります。)例外がキャッチされた場合、not再スローされるか、原因としてラップされている場合にのみ、スタックトレースをログに記録します別の例外。スタックトレースとラップされた例外のメッセージは、いずれにせよ「原因」としてスタックトレースに記録されます。また、キャッチャーは、たとえば、最初の失敗時にエラーをログに記録せずに再試行するか、単にそれを警告として扱うかなど。
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
}
}
誰かがGetNumberAndReturnCode
を呼び出し、500
コードを受け取り、エラーを通知したとします。彼はサポートに連絡し、ログファイルを開いてこれを確認します。
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の値を要求しているため、要求が無効であるとユーザーに単に応答することができます。
例外を処理する場合は、正しい方法で処理してください。また、例外が正しいデータ/メッセージを最初に含み、アーキテクチャレイヤーに従っていることを確認してください。そうすれば、メッセージが、発生する可能性のある問題を特定するのに役立ちます。
これはアンチパターンです。
私の意見では、例外のコンストラクターでロギング呼び出しを行うことは、次の例のようになります: 欠陥:コンストラクターが実際の作業を行う 。
コンストラクターが外部サービス呼び出しを行うことを期待したり、望んだりすることは決してありません。これは非常に望ましくない副作用であり、MiškoHeveryが指摘するように、サブクラスとモックに不要な動作を継承させます。
そのため、それは 最小の驚きの原則 にも違反します。
他の人と一緒にアプリケーションを開発している場合、この副作用はおそらく他の人にはわかりません。一人で作業している場合でも、そのことを忘れて、自分を驚かせてしまうかもしれません。