委託提供一種機制,讓軟體設計能在元件之間保持最低程度的耦合。
這種設計的絕佳範例之一是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。 但是,由於記錄器與任何輸出引擎之間的結合只會透過委派,因此不會涉及任何其他類型(例如介面或基類)。 耦合要盡可能小。