Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Делегаты предоставляют механизм, обеспечивающий разработку программного обеспечения с минимальным взаимодействием между компонентами.
Одним из отличных примеров для такого вида дизайна является 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 — мощный шаблон, который стоит изучить и понять.
В этом простом примере показано, как делегаты требуют очень мало взаимодействия между компонентами. Вам не нужно создавать класс, производный от определенного базового класса. Вам не нужно реализовать определенный интерфейс. Единственным требованием является предоставление реализации одного метода, который является фундаментальным для задачи.
Постройте свои собственные компоненты с помощью делегатов
Давайте продолжим развитие этого примера, создав компонент, используя дизайн, который зависит от делегатов.
Определим компонент, который можно использовать для сообщений журнала в большой системе. Компоненты библиотеки можно использовать во многих разных средах на нескольких разных платформах. В компоненте, который управляет журналами, существует множество распространенных функций. Он должен принимать сообщения из любого компонента в системе. Эти сообщения будут иметь разные приоритеты, которым может управлять основной компонент. Сообщения должны иметь метки времени в окончательной архивной форме. Для более сложных сценариев можно фильтровать сообщения по исходному компоненту.
Существует один аспект функции, которая часто меняется: где записываются сообщения. В некоторых средах они могут быть записаны в консоль ошибок. В других - файл. Другие возможности включают хранилище баз данных, журналы событий ОС или другое хранилище документов.
Существуют также сочетания выходных данных, которые могут использоваться в разных сценариях. Может потребоваться записать сообщения в консоль и в файл.
Проектирование, основанное на делегатах, обеспечит значительную гибкость и облегчит поддержку механизмов хранения, которые могут быть добавлены в будущем.
В этой структуре основной компонент журнала может быть не виртуальным, даже закрытым классом. Вы можете подключить любой набор делегатов для записи сообщений в различные средства хранения. Встроенная поддержка мультикастовых делегатов облегчает создание сценариев, в которых сообщения должны выводиться в несколько точек (например, в файл и консоль).
Первая реализация
Давайте начнем с малого: начальная реализация будет принимать новые сообщения и записывать их с помощью любого присоединенного делегата. Вы можете начать с одного делегата, который записывает сообщения в консоль.
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
не имеет присоединенного списка вызовов.
Вы можете предпочесть дизайн, который незаметно продолжается, если методы не были подключены. Это легко использовать условный оператор NULL, в сочетании с методом Delegate.Invoke()
:
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
Оператор условного нуля (?.
) выполняет короткое замыкание, когда левый операнд (WriteMessage
в данном случае) имеет значение NULL, что означает, что попытка записать сообщение не предпринимается.
Вы не найдете метод Invoke()
, перечисленный в документации для System.Delegate
или System.MulticastDelegate
. Компилятор создает безопасный Invoke
метод типа для любого объявленного типа делегата. В этом примере это означает, что Invoke
принимает один аргумент string
и имеет тип возвращаемого значения void.
Сводка методик
Вы видели начало компонента журнала, который можно расширить с помощью других средств записи и других функций. Используя делегаты при разработке, эти различные компоненты слабо связаны между собой. Это дает несколько преимуществ. Легко создать новые механизмы вывода и подключить их к системе. Эти другие механизмы требуют только одного метода: метод, который записывает сообщение журнала. Это дизайн, который устойчив при добавлении новых функций. Требуемый контракт для любого автора заключается в реализации одного метода. Этот метод может быть статическим или экземплярным методом. Это может быть общедоступный, частный или любой другой юридический доступ.
Класс Logger может вносить любое количество улучшений или изменений без внесения критических изменений. Как и любой класс программирования, нельзя изменять общедоступный API без риска изменений, которые могут привести к поломкам. Но, так как связь между средством ведения журнала и любыми обработчиками выходных данных осуществляется только через делегат, другие типы (например, интерфейсы или базовые классы) не участвуют. Муфта настолько мала, насколько это возможно.