Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Los delegados proporcionan un mecanismo que permite diseños de software que implican un acoplamiento mínimo entre componentes.
Un excelente ejemplo para este tipo de diseño es LINQ. El patrón de expresión de consulta LINQ se basa en delegados para todas sus características. Considere este ejemplo sencillo:
var smallNumbers = numbers.Where(n => n < 10);
Esto filtra la secuencia de números solo a los menores que el valor 10.
El Where método usa un delegado que determina qué elementos de una secuencia pasan el filtro. Cuando crea una consulta LINQ, proporciona la implementación del delegado para este fin específico.
El prototipo del método Where es:
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);
Este ejemplo se repite con todos los métodos que forman parte de LINQ. Todos confían en los delegados para gestionar el código de la consulta específica. Este patrón de diseño de API es un poderoso recurso para aprender y comprender.
En este ejemplo sencillo se muestra cómo los delegados requieren muy poco acoplamiento entre componentes. No es necesario crear una clase que derive de una clase base determinada. No es necesario implementar una interfaz específica. El único requisito es proporcionar la implementación de un método fundamental para la tarea a mano.
Creación de sus propios componentes con delegados
Vamos a basarnos en ese ejemplo creando un componente utilizando un diseño que se basa en delegados.
Vamos a definir un componente que se podría usar para los mensajes de registro en un sistema grande. Los componentes de biblioteca se pueden usar en muchos entornos diferentes, en varias plataformas diferentes. Hay muchas características comunes en el componente que administra los registros. Tendrá que aceptar mensajes de cualquier componente del sistema. Esos mensajes tendrán prioridades diferentes, que el componente principal puede administrar. Los mensajes deben tener marcas de tiempo en su formulario archivado final. Para escenarios más avanzados, puede filtrar los mensajes por el componente de origen.
Hay un aspecto de la característica que cambiará a menudo: donde se escriben los mensajes. En algunos entornos, pueden escribirse en la consola de errores. En otros, un archivo. Otras posibilidades son el almacenamiento de bases de datos, los registros de eventos del sistema operativo u otro almacenamiento de documentos.
También hay combinaciones de salida que se pueden usar en distintos escenarios. Es posible que quiera escribir mensajes en la consola y en un archivo.
Un diseño basado en delegados proporcionará una gran flexibilidad y facilitará la compatibilidad con los mecanismos de almacenamiento que se pueden agregar en el futuro.
En este diseño, el componente de registro principal puede ser una clase no virtual, incluso sellada. Puede conectar cualquier conjunto de delegados para escribir los mensajes en distintos medios de almacenamiento. La compatibilidad integrada con delegados de multidifusión facilita la compatibilidad con escenarios en los que los mensajes deben escribirse en varias ubicaciones (un archivo y una consola).
Una primera implementación
Comencemos pequeño: la implementación inicial aceptará nuevos mensajes y los escribirá con cualquier delegado adjunto. Puede empezar con un delegado que escriba mensajes en la consola.
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(string msg)
{
if (WriteMessage is not null)
WriteMessage(msg);
}
}
La clase estática anterior es lo más sencillo que puede funcionar. Es necesario escribir la implementación única para el método que escribe mensajes en la consola:
public static class LoggingMethods
{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}
Por último, necesita conectar el delegado asociándolo al delegado WriteMessage que se declara en el registrador:
Logger.WriteMessage += LoggingMethods.LogToConsole;
Prácticas
Nuestro ejemplo hasta ahora es bastante simple, pero aún demuestra algunas de las directrices importantes para los diseños que involucran delegados.
El uso de los tipos de delegado definidos en el marco principal facilita el trabajo de los usuarios con los delegados. No es necesario definir nuevos tipos y los desarrolladores que usan la biblioteca no necesitan aprender nuevos tipos de delegado especializados.
Las interfaces usadas son lo más mínimas y flexibles posibles: para crear un nuevo registrador de salida, debe crear un método. Ese método puede ser un método estático o un método de instancia. Puede tener cualquier acceso.
Formato de salida
Vamos a hacer que esta primera versión sea un poco más sólida y, a continuación, empiece a crear otros mecanismos de registro.
A continuación, vamos a agregar algunos argumentos al método para que la LogMessage() clase de registro cree mensajes más estructurados:
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);
}
}
A continuación, vamos a usar ese Severity argumento para filtrar los mensajes que se envían a la salida del registro.
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);
}
}
Prácticas
Ha agregado nuevas funcionalidades a la infraestructura de registro. Dado que el componente del registrador está muy acoplado de forma flexible a cualquier mecanismo de salida, estas nuevas características se pueden agregar sin afectar a ninguno de los códigos que implementan el delegado del registrador.
A medida que siga compilando esto, verá más ejemplos de cómo este acoplamiento flexible permite una mayor flexibilidad en la actualización de partes del sitio sin cambios en otras ubicaciones. De hecho, en una aplicación mayor, las clases de salida del registrador podrían estar en un ensamblado diferente y es posible que no necesiten ser recompiladas.
Creación de un segundo motor de salida
El componente Log está avanzando bien. Vamos a agregar un motor de salida más que registra los mensajes en un archivo. Este será un motor de salida ligeramente más involucrado. Será una clase que encapsula las operaciones de archivo y garantiza que el archivo esté siempre cerrado después de cada escritura. Esto garantiza que todos los datos se vacían en el disco después de generar cada mensaje.
Este es el registrador basado en archivos:
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.
}
}
}
Una vez creada esta clase, puede crear una instancia de ella y adjuntar su método LogMessage al componente Logger:
var file = new FileLogger("log.txt");
Estos dos no son mutuamente excluyentes. Puede adjuntar métodos de registro y generar mensajes en la consola y en un archivo:
var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier
Después, incluso en la misma aplicación, puede quitar uno de los delegados sin ocasionar ningún otro problema en el sistema:
Logger.WriteMessage -= LoggingMethods.LogToConsole;
Prácticas
Ahora, ha agregado un segundo controlador de salida para el subsistema de registro. Esta necesita infraestructura adicional para dar soporte adecuado al sistema de archivos. El delegado es un método de instancia. También es un método privado. No es necesario tener una mayor accesibilidad porque la infraestructura del delegado puede conectar los delegados.
En segundo lugar, el diseño basado en delegados habilita varios métodos de salida sin código adicional. No es necesario crear ninguna infraestructura adicional para admitir varios métodos de salida. Simplemente se convierten en otro método en la lista de invocación.
Preste especial atención al código en el método de salida de registros de archivos. Se codifica para asegurarse de que no produce ninguna excepción. Aunque esto no siempre es estrictamente necesario, a menudo es una buena práctica. Si cualquiera de los métodos de delegado produce una excepción, los delegados restantes que se encuentran en la invocación no se invocarán.
Como última nota, el registrador de archivos debe administrar sus recursos abriendo y cerrando el archivo en cada mensaje de registro. Puede optar por mantener el archivo abierto e implementar IDisposable para cerrar el archivo cuando haya terminado.
Cualquiera de los métodos tiene sus ventajas y desventajas. Ambos crean un poco más de acoplamiento entre las clases.
Ninguno de los códigos de la Logger clase tendría que actualizarse para admitir cualquiera de los escenarios.
Control de delegados null
Por último, vamos a actualizar el método LogMessage para que sea sólido para esos casos cuando no se selecciona ningún mecanismo de salida. La implementación actual generará una NullReferenceException cuando el WriteMessage delegado no tenga una lista de invocación asociada.
Es posible que prefiera un diseño que continúe silenciosamente cuando no se haya adjuntado ningún método. Esto es fácil mediante el operador condicional NULL, combinado con el Delegate.Invoke() método :
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
El operador condicional nulo (?.) cortocircuita cuando el operando izquierdo (WriteMessage en este caso) es nulo, lo que significa que no se intenta registrar ningún mensaje.
No va a encontrar el método Invoke() que figura en la documentación de System.Delegate o System.MulticastDelegate. El compilador genera un método seguro de tipos Invoke para cualquier tipo de delegado declarado. En este ejemplo, esto significa que Invoke toma un único argumento string y tiene un tipo de retorno void.
Resumen de prácticas
Ha observado los comienzos de un componente de registro que puede expandirse con otros sistemas de escritura y otras características. Mediante el uso de delegados en el diseño, estos distintos componentes están acoplados de forma flexible. Esto proporciona varias ventajas. Es fácil crear nuevos mecanismos de salida y adjuntarlos al sistema. Estos otros mecanismos solo necesitan un método: el método que escribe el mensaje de registro. Es un diseño resistente cuando se agregan nuevas características. El contrato requerido para cualquier escritor consiste en implementar un método. Ese método podría ser un método estático o de instancia. Podría ser público, privado o cualquier otro acceso legal.
La clase Logger puede realizar cualquier número de mejoras o cambios sin introducir cambios importantes. Al igual que cualquier clase, no se puede modificar la API pública sin incurrir en el riesgo de cambios disruptivos. Pero, como el acoplamiento entre el registrador y cualquier motor de salida se realiza solo mediante el delegado, ningún otro tipo (como interfaces o clases base) está involucrado. El acoplamiento es lo más pequeño posible.