1. ホーム
  2. c#

[解決済み] ロガーラッパーのベストプラクティス

2023-03-09 16:38:11

質問

アプリケーションでnloggerを使用したいのですが、将来的にロギングシステムを変更する必要があるかもしれません。 そこで、私はロギングファサードを使用したいと思います。

それらをどのように書くか、既存の例のための任意の推奨事項を知っていますか? または、この分野におけるいくつかのベストプラクティスへのリンクを教えてください。

どのように解決するのですか?

私は以前、以下のようなロギングファサードを使っていました。 共通.ロギング を使用していました (私自身の CuttingEdge.Logging(カッティングエッジ.ロギング ライブラリを隠すこともできます)、しかし最近は Dependency Injectionパターン . これにより、ロガーをアプリケーション定義の抽象化の背後に隠すことができます。 依存性逆転の原則 インターフェース分離の原則 (ISP)の原則に従ったものです。

アプリケーションの中核部分が外部ライブラリの存在について持つ知識を最小化することは、たとえロギングライブラリを決して置き換えるつもりがないとしても、より良いことです。外部ライブラリへのハード依存は、コードのテストをより困難にし、アプリケーションのために特別に設計されていない API でアプリケーションを複雑にしてしまいます。

私のアプリケーションでは、抽象化はしばしばこのように見えます。

public interface ILogger
{
    void Log(LogEntry entry);
}

public sealed class ConsoleLogger : ILogger
{
    public void Log(LogEntry entry)
}

public enum LoggingEventType { Debug, Information, Warning, Error, Fatal };

// Immutable DTO that contains the log information.
public struct LogEntry
{
    public LoggingEventType Severity { get; }
    public string Message { get; }
    public Exception Exception { get; }

    public LogEntry(LoggingEventType severity, string msg, Exception ex = null)
    {
        if (msg is null) throw new ArgumentNullException("msg");
        if (msg == string.Empty) throw new ArgumentException("empty", "msg");

        this.Severity = severity;
        this.Message = msg;
        this.Exception = ex;
    }
}

オプションとして、この抽象化はいくつかの簡単な拡張メソッドで拡張することができます(インターフェイスが狭いままで、ISPに準拠し続けることを可能にします)。これは、このインタフェースの消費者のためのコードをよりシンプルにします。

public static class LoggerExtensions
{
    public static void Log(this ILogger logger, string message) =>
        logger.Log(new LogEntry(LoggingEventType.Information, message));

    public static void Log(this ILogger logger, Exception ex) =>
        logger.Log(new LogEntry(LoggingEventType.Error, ex.Message, ex));

    // More methods here.
}

このインターフェイスにはメソッドが1つしかないので、簡単に ILogger という実装を簡単に作ることができます。 log4net へのプロキシ , からSerilogへ , Microsoft.Extensions.Logging NLog などのロギングライブラリを使用し、DIコンテナ内で ILogger を持つクラスに注入するように DI コンテナを設定します。また、以下のリストのように、コンソールに書き込む実装や、ユニットテストに使用できる偽の実装を簡単に作成することができます。

public class ConsoleLogger : ILogger
{
    public void Log(LogEntry entry) => Console.WriteLine(
      $"[{entry.Severity}] {DateTime.Now} {entry.Message} {entry.Exception}");
}

public class FakeLogger : List<LogEntry>, ILogger
{
    public void Log(LogEntry entry) => this.Add(entry);
}

単一のメソッドを持つインターフェースの上に静的な拡張メソッドを持つことは、多くのメンバーを持つインターフェースとは全く異なります。拡張メソッドは単なるヘルパーメソッドであり、このメソッドによって LogEntry メッセージの唯一のメソッドである ILogger インターフェース上の唯一のメソッドに渡します。これらの拡張メソッド自体はそれ自身のVolatile Behaviorを含まないので、テスト容易性を妨げることはありません。望むなら簡単にテストできますし、抽象化の一部ではなく、消費者のコードの一部となります。

これによって、抽象化を変更することなく拡張メソッドを進化させることができるだけでなく、拡張メソッドと LogEntry コンストラクタは、ロガーがスタブ/モック化されている場合でも、ロガー抽象化が使用されるときに常に実行されます。これにより、テストスイートで実行するときに、ロガーへの呼び出しの正しさについてより確実なものになります。使用されるサードパーティのロガー抽象化への私の呼び出しがユニットテストの間に成功したが、実稼働で実行されたときにまだ失敗したとき、私はこれで何度も自分自身を撃ってきました。

多くのメンバーを持つ抽象化は、(モック、アダプター、およびデコレーターのような)実装を作成することを難しくします。

このようにすると、ロギングファサード(または他のライブラリ)が提供するかもしれない静的な抽象化の必要性はほとんどありません。

それでも、この ILogger への依存を必要とするクラスが少なくなるようにアプリケーションを設計することをお勧めします。 ILogger への依存を必要とするクラスが少なくなるようなアプリケーションを設計することが望ましいです。 この回答 はこのことについて詳しく述べています。