次の方法で共有


デリゲートの一般的なパターン

前の

デリゲートは、コンポーネント間の結合を最小限に抑えるソフトウェア設計を可能にするメカニズムを提供します。

この種のデザインの優れた例の 1 つは LINQ です。 LINQ クエリ式パターンは、すべての機能のデリゲートに依存します。 この簡単な例を考えてみましょう。

var smallNumbers = numbers.Where(n => n < 10);

これにより、数値のシーケンスが値 10 未満のみにフィルター処理されます。 Where メソッドは、フィルターを渡すシーケンスの要素を決定するデリゲートを使用します。 LINQ クエリを作成するときは、この特定の目的のためにデリゲートの実装を指定します。

Where メソッドのプロトタイプは次のとおりです。

public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);

この例は、LINQ の一部であるすべてのメソッドで繰り返されます。 これらはすべて、特定のクエリを管理するコードのデリゲートに依存します。 この API 設計パターンは、学習して理解するための強力なパターンです。

この簡単な例は、デリゲートがコンポーネント間の結合をほとんど必要としない方法を示しています。 特定の基底クラスから派生するクラスを作成する必要はありません。 特定のインターフェイスを実装する必要はありません。 唯一の要件は、目の前のタスクの基礎となる 1 つのメソッドの実装を提供することです。

デリゲートを使用して独自のコンポーネントを構築する

その例に基づいて、デリゲートに依存するデザインを使用してコンポーネントを作成しましょう。

大規模なシステムのログ メッセージに使用できるコンポーネントを定義しましょう。 ライブラリ コンポーネントは、複数の異なるプラットフォーム上のさまざまな環境で使用できます。 ログを管理するコンポーネントには、多くの一般的な機能があります。 システム内の任意のコンポーネントからのメッセージを受け入れる必要があります。 これらのメッセージには、コア コンポーネントが管理できる優先順位が異なります。 メッセージには、最終的なアーカイブ形式のタイムスタンプが含まれている必要があります。 より高度なシナリオでは、ソース コンポーネントによってメッセージをフィルター処理できます。

頻繁に変更される機能には、メッセージが書き込まれる場所という 1 つの側面があります。 一部の環境では、エラー コンソールに書き込まれる場合があります。 ファイルに出力されるかは、環境によってさまざまです。 その他の可能性としては、データベース ストレージ、OS イベント ログ、またはその他のドキュメント ストレージがあります。

また、さまざまなシナリオで使用できる出力の組み合わせもあります。 コンソールとファイルにメッセージを書き込む場合があります。

デリゲートに基づく設計により、柔軟性が大幅に向上し、将来的に追加される可能性のあるストレージ メカニズムを簡単にサポートできるようになります。

この設計では、プライマリ ログ コンポーネントは非仮想クラスであってもシール クラスでもかまいません。 任意のデリゲート セットをプラグインして、メッセージを別のストレージ メディアに書き込むことができます。 マルチキャスト デリゲートの組み込みサポートにより、メッセージを複数の場所 (ファイルとコンソール) に書き込む必要があるシナリオを簡単にサポートできます。

最初の実装

小規模から始めましょう。最初の実装では新しいメッセージを受け入れ、添付されたデリゲートを使用して書き込みます。 コンソールにメッセージを書き込む 1 つのデリゲートから始めることができます。

public static class Logger
{
    public static Action<string>? WriteMessage;

    public static void LogMessage(string msg)
    {
        if (WriteMessage is not null)
            WriteMessage(msg);
    }
}

上記の静的クラスは、動作できる最も簡単なものです。 コンソールにメッセージを書き込むメソッドの単一の実装を記述する必要があります。

public static class LoggingMethods
{
    public static void LogToConsole(string message)
    {
        Console.Error.WriteLine(message);
    }
}

最後にそのメソッドを、Logger に宣言されている WriteMessage デリゲートにアタッチして接続する必要があります。

Logger.WriteMessage += LoggingMethods.LogToConsole;

プラクティス

ここまでのサンプルは非常に単純ですが、デリゲートを含むデザインの重要なガイドラインの一部を示しています。

コア フレームワークで定義されているデリゲート型を使用すると、ユーザーはデリゲートを簡単に操作できます。 新しい型を定義する必要はありません。ライブラリを使用する開発者は、新しい特殊なデリゲート型を学習する必要はありません。

使用されるインターフェイスは、可能な限り最小限かつ柔軟です。新しい出力ロガーを作成するには、1 つのメソッドを作成する必要があります。 そのメソッドには、静的メソッドまたはインスタンス メソッドを使用できます。 またアクセス指定も任意です。

出力を整形

この最初のバージョンを少し堅牢にしてから、他のログメカニズムの作成を開始しましょう。

次に、ログ クラスがより構造化されたメッセージを作成できるように、 LogMessage() メソッドにいくつかの引数を追加します。

public enum Severity
{
    Verbose,
    Trace,
    Information,
    Warning,
    Error,
    Critical
}
public static class Logger
{
    public static Action<string>? WriteMessage;

    public static void LogMessage(Severity s, string component, string msg)
    {
        var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
        if (WriteMessage is not null)
            WriteMessage(outputMsg);
    }
}

次に、その Severity 引数を使用して、ログの出力に送信されるメッセージをフィルター処理してみましょう。

public static class Logger
{
    public static Action<string>? WriteMessage;

    public static Severity LogLevel { get; set; } = Severity.Warning;

    public static void LogMessage(Severity s, string component, string msg)
    {
        if (s < LogLevel)
            return;

        var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
        if (WriteMessage is not null)
            WriteMessage(outputMsg);
    }
}

プラクティス

ログ 記録インフラストラクチャに新しい機能が追加されました。 ロガー コンポーネントは任意の出力メカニズムに非常に疎結合されているため、ロガー デリゲートを実装するコードに影響を与えずに、これらの新機能を追加できます。

これを構築し続けるにつれて、この疎結合によって、他の場所に変更を加えずにサイトの一部を更新する際の柔軟性が向上する方法の例が増えます。 実際、大規模なアプリケーションでは、ロガー出力クラスが別のアセンブリ内にあり、再構築する必要さえありません。

2 つ目の出力エンジンを構築する

ログコンポーネントは順調に進んでいます。 メッセージをファイルに記録する出力エンジンをもう 1 つ追加しましょう。 これは、少し複雑な出力エンジンになります。 ファイル操作をカプセル化し、書き込みのたびに常にファイルが閉じられるようにするクラスになります。 これにより、各メッセージが生成された後にすべてのデータがディスクにフラッシュされます。

ファイル ベースのロガーを次に示します。

public class FileLogger
{
    private readonly string logPath;
    public FileLogger(string path)
    {
        logPath = path;
        Logger.WriteMessage += LogMessage;
    }

    public void DetachLog() => Logger.WriteMessage -= LogMessage;
    // make sure this can't throw.
    private void LogMessage(string msg)
    {
        try
        {
            using (var log = File.AppendText(logPath))
            {
                log.WriteLine(msg);
                log.Flush();
            }
        }
        catch (Exception)
        {
            // Hmm. We caught an exception while
            // logging. We can't really log the
            // problem (since it's the log that's failing).
            // So, while normally, catching an exception
            // and doing nothing isn't wise, it's really the
            // only reasonable option here.
        }
    }
}

このクラスを作成したら、それをインスタンス化し、LogMessage メソッドを Logger コンポーネントにアタッチします。

var file = new FileLogger("log.txt");

これら 2 つは相互に排他的ではありません。 ログ メソッドの両方をアタッチし、コンソールとファイルにメッセージを生成できます。

var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier

後で、同じアプリケーションでも、システムに他の問題を発生させずに、デリゲートの 1 つを削除できます。

Logger.WriteMessage -= LoggingMethods.LogToConsole;

プラクティス

次に、ログ サブシステムの 2 つ目の出力ハンドラーを追加しました。 ファイル システムを正しくサポートするには、もう少しインフラストラクチャが必要です。 デリゲートはインスタンス メソッドです。 プライベート メソッドでもあります。 デリゲート インフラストラクチャはデリゲートを接続できるため、アクセシビリティを高める必要はありません。

次に、デリゲート ベースの設計により、追加のコードなしで複数の出力メソッドが有効になります。 複数の出力方法をサポートするために、追加のインフラストラクチャを構築する必要はありません。 これらは単に呼び出しリストの別のメソッドになります。

ファイル ログ出力方法のコードには特に注意してください。 例外が発生しないようにコード化されており、問題なく動作します。 これは必ずしも厳密には必要ではありませんが、多くの場合、良い方法です。 デリゲート メソッドのいずれかが例外をスローした場合、呼び出し中の残りのデリゲートは呼び出されません。

最後の注意事項として、ファイル ロガーは、各ログ メッセージでファイルを開いたり閉じたりして、リソースを管理する必要があります。 ファイルを開いたままにし、完了したらファイルを閉じる IDisposable を実装することもできます。 どちらの方法にも長所と短所があります。 どちらもクラス間の結合度を少し高めます。

どちらのシナリオもサポートするために、 Logger クラスのコードを更新する必要はありません。

Null デリゲートを処理する

最後に、LogMessage メソッドを更新して、出力メカニズムが選択されていない場合に堅牢になるようにしましょう。 現在の実装コードでは、NullReferenceException デリゲートに呼び出しリストがアタッチされなかった場合、WriteMessage がスローされます。 メソッドがアタッチされていない場合は、静かに続行するデザインを好むかもしれません。 これは、null 条件演算子と Delegate.Invoke() メソッドを組み合わせて使用すると簡単です。

public static void LogMessage(string msg)
{
    WriteMessage?.Invoke(msg);
}

左側のオペランド (?.) が null の場合、null 条件演算子 (WriteMessage) はショートサーキットし、メッセージのログを行いません。

Invoke()またはSystem.Delegateのドキュメントに記載されているSystem.MulticastDelegateメソッドは見つかりません。 コンパイラは、宣言されたデリゲート型の型セーフ Invoke メソッドを生成します。 この例では、 Invoke は 1 つの string 引数を受け取り、void 戻り値の型を持っていることを意味します。

プラクティスの概要

ログ コンポーネントの基本的概念を見てきました。この概念は、その他の出力機構や各種機能に応用することができます。 デザインでデリゲートを使用すると、これらの異なるコンポーネントが疎結合されます。 これにはいくつかの利点があります。 新しい出力メカニズムを作成してシステムにアタッチするのは簡単です。 これらの他のメカニズムには、ログ メッセージを書き込むメソッドという 1 つのメソッドのみが必要です。 これは、新機能が追加されたときに回復性がある設計です。 ライターに必要なコントラクトは、1 つのメソッドを実装することです。 そのメソッドには、静的メソッドまたはインスタンス メソッドを指定できます。 パブリック、プライベート、またはその他の法的アクセスである可能性があります。

Logger クラスは、破壊的変更を加えることなく、任意の数の拡張機能や変更を行うことができます。 他のクラスと同様に、変更を中断するリスクなしにパブリック API を変更することはできません。 ただし、ロガーと出力エンジンの間の結合はデリゲートを介してのみ行われるため、他の型 (インターフェイスや基底クラスなど) は関係しません。 その結合度は最小限で済むのです。

次に