共用方式為


委派的一般模式

以前

委託提供一種機制,讓軟體設計能在元件之間保持最低程度的耦合。

這種設計的絕佳範例之一是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 設計模式是學習和了解的強大設計模式。

這個簡單的範例說明代理在组件之間需要很少的耦合。 您不需要建立衍生自特定基類的類別。 您不需要實作特定的介面。 唯一的要求是提供一個對當前任務至關重要的方法的實現。

使用委派建立您自己的元件

讓我們以依賴委派的設計來創建一個元件,進一步建構範例。

讓我們定義可用於大型系統中記錄訊息的元件。 程式庫元件可以被使用於多個不同平臺的許多不同環境中。 管理記錄的元件中有許多常見的功能。 它必須接受系統中任何元件的訊息。 這些訊息會有不同的優先順序,核心元件可以管理這些優先順序。 訊息在最終封存的形式中應包含時間戳。 針對更進階的案例,您可以依來源元件篩選訊息。

功能有一個層面會經常變更:寫入訊息的位置。 在某些環境中,資訊可能會寫入錯誤控制台。 其他事項中有一個檔案。 其他可能性包括資料庫記憶體、OS 事件記錄檔或其他文件記憶體。

也有可用於不同案例的輸出組合。 您可能想要將訊息寫入主控台和檔案。

以委派為基礎的設計會提供大量的彈性,並讓您輕鬆地支持未來可能新增的儲存機制。

在此設計下,主要記錄元件可以是非虛擬,甚至是密封類別。 您可以利用任何一組委派函式,將訊息寫入不同的儲存媒體。 多播委派的內建支援可讓您輕鬆支援訊息必須寫入多個位置的情境,例如檔案和控制台。

第一個實作

讓我們從小處著手:初始實作會接受新訊息,並使用任何附加的委派對象加以撰寫。 您可以從一個將訊息寫入控制台的委派開始。

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

最後,您必須將委派連接至記錄器中宣告的 WriteMessage 委派,以實現委派連接:

Logger.WriteMessage += LoggingMethods.LogToConsole;

實踐

到目前為止,我們的範例相當簡單,但仍示範一些涉及委派的設計重要指導方針。

使用核心架構中定義的委派類型,可讓使用者更輕鬆地使用委派。 您不需要定義新的類型,而使用連結庫的開發人員不需要學習新的特製化委派類型。

所使用的介面盡可能最少且具有彈性:若要建立新的輸出記錄器,您必須建立一個方法。 該方法可能是靜態方法或實例方法。 它可能擁有任何類型的存取權限。

格式化輸出

讓我們讓第一個版本更健全,然後開始建立其他記錄機制。

接下來,讓我們將幾個自變數新增至 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);
    }
}

實踐

您已將新功能添加到記錄基礎設施中。 由於記錄器元件與任何輸出機制非常鬆散結合,因此可以加入這些新功能,而不會影響任何實作記錄器委派的程序代碼。

當您持續建置此專案時,您會看到更多範例,說明此鬆散結合如何讓您在更新網站部分時有更大的彈性,而不需要對其他位置進行任何變更。 事實上,在較大的應用程式中,記錄器輸出類別可能位於不同的元件中,甚至不需要重建。

建置第二個輸出引擎

記錄元件進展順利。 讓我們再新增一個將訊息記錄至檔案的輸出引擎。 這將是稍微更複雜的輸出引擎。 它會是封裝檔案作業的類別,並確保檔案在每次寫入之後一律關閉。 這可確保在產生每個訊息之後,所有數據都會排清到磁碟。

這是檔案型記錄器:

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");

這兩者不互斥。 您可以將記錄方法附加至主控台和檔案並產生訊息:

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

之後,即使在相同的應用程式中,您也可以移除其中一個委派,而不需要系統發生任何其他問題:

Logger.WriteMessage -= LoggingMethods.LogToConsole;

實踐

現在,您已新增記錄子系統的第二個輸出處理程式。 此項目需要更多基礎設施才能正確支援檔案系統。 委派函式是實例方法。 這也是私人方法。 不需要更高的可及性,因為委派基礎設施可以連接委派。

其次,委派型設計會啟用多個輸出方法,而不需要任何額外的程序代碼。 您不需要建置任何其他基礎結構以支援多個輸出方法。 它們只會成為調用清單中的另一個方法。

請特別注意檔案記錄輸出方法中的程序代碼。 它會被編碼以確保不會拋出任何異常。 雖然這不一定是絕對必要的,但通常是一個很好的做法。 如果任一委派方法擲回例外狀況,則調用上的其餘的委派方法不會被執行。

最後請注意,檔案記錄器必須開啟和關閉每個記錄訊息上的檔案,以管理其資源。 您可以選擇讓檔案保持開啟,並在完成時實 IDisposable 作 以關閉檔案。 任一方法都有其優點和缺點。 這兩者都會使類別之間的耦合增加一點。

類別中的 Logger 程序代碼都不需要更新,才能支援任一案例。

處理 Null 委託

最後,讓我們更新 LogMessage 方法,以確保在未選取任何輸出機制時,它的運作更加強健。 當 NullReferenceException 委派沒有附加調用清單時,當前的實作會拋出 WriteMessage。 您可能會偏好在未附加任何方法時繼續工作的靜默設計。 這很容易使用空條件運算子,並結合 Delegate.Invoke() 方法來實現:

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

當左運算元(在此案例中)為 null 時,null 條件運算子(?.)會短路,這表示不會嘗試記錄訊息。

您不會在 Invoke()System.Delegate 的文件中找到列出的 System.MulticastDelegate 方法。 編譯程式會針對任何宣告的委派類型產生型別安全 Invoke 方法。 在此範例中,這表示 Invoke 接受單一 string 參數,並且具有 void 傳回類型。

實務摘要

您已看到日誌元件的初始版本,可以擴展為包含其他寫入器和功能。 透過在設計中使用委派,這些不同的元件會鬆散結合。 這提供數個優點。 建立新的輸出機制並將它們附加至系統很容易。 這些其他機制只需要一種方法:寫入記錄訊息的方法。 這是新增新功能時具有復原性的設計。 任何寫入器要求的合約是執行一個方法。 該方法可以是靜態或實例方法。 它可以是公用、私人或任何其他合法存取權。

Logger 類別可以進行任意數目的增強功能或變更,而不需要引入重大變更。 如同任何類別,若不冒著重大變更的風險,您無法修改公用 API。 但是,由於記錄器與任何輸出引擎之間的結合只會透過委派,因此不會涉及任何其他類型(例如介面或基類)。 耦合要盡可能小。

下一步