Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Os delegados fornecem um mecanismo que permite designs de software que envolvem acoplamento mínimo entre componentes.
Um excelente exemplo para esse tipo de design é LINQ. O padrão de expressão de consulta LINQ depende de delegados em todas as suas funcionalidades. Considere este exemplo simples:
var smallNumbers = numbers.Where(n => n < 10);
Isso filtra a sequência de números apenas para aqueles menores que o valor 10.
O Where método usa um delegado que determina quais elementos de uma sequência passam o filtro. Ao criar uma consulta LINQ, você fornece a implementação do delegado para essa finalidade específica.
O protótipo do método Where é:
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);
Este exemplo é repetido com todos os métodos que fazem parte do LINQ. Todos eles dependem de delegados para o código que gerencia a consulta específica. Esse padrão de design de API é poderoso para aprender e entender.
Este exemplo simples ilustra como os delegados exigem muito pouco acoplamento entre componentes. Você não precisa criar uma classe derivada de uma classe base específica. Você não precisa implementar uma interface específica. O único requisito é fornecer a implementação de um método fundamental para a tarefa em questão.
Criar componentes próprios com delegados
Vamos aprimorar esse exemplo criando um componente que utiliza um design baseado em delegados.
Vamos definir um componente que pode ser usado para mensagens de log em um sistema grande. Os componentes da biblioteca podem ser usados em vários ambientes diferentes, em várias plataformas diferentes. Há muitos recursos comuns no componente que gerencia os logs. Ele precisará aceitar mensagens de qualquer componente no sistema. Essas mensagens terão prioridades diferentes, que o componente principal pode gerenciar. As mensagens devem ter carimbos de data/hora na sua forma final arquivada. Para cenários mais avançados, você pode filtrar mensagens pelo componente de origem.
Há um aspecto do recurso que mudará com frequência: onde as mensagens são gravadas. Em alguns ambientes, eles podem ser gravados no console de erros. Em outros, um arquivo. Outras possibilidades incluem armazenamento de banco de dados, logs de eventos do sistema operacional ou outro armazenamento de documentos.
Também há combinações de saída que podem ser usadas em cenários diferentes. Talvez você queira gravar mensagens no console e em um arquivo.
Um design baseado em delegados fornecerá muita flexibilidade e facilitará o suporte a mecanismos de armazenamento que podem ser adicionados no futuro.
Nesse design, o componente de log primário pode ser uma classe não virtual, até mesmo selada. Você pode conectar qualquer conjunto de delegados para gravar as mensagens em diferentes mídias de armazenamento. O suporte interno para delegados multicast facilita o suporte a cenários em que as mensagens devem ser gravadas em vários locais (um arquivo e um console).
Uma primeira implementação
Vamos começar com algo pequeno: a nova implementação aceitará novas mensagens e as gravará usando qualquer delegado vinculado. Você pode começar com um delegado que grava mensagens no console.
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(string msg)
{
if (WriteMessage is not null)
WriteMessage(msg);
}
}
A classe estática acima é a coisa mais simples que pode funcionar. Precisamos gravar a implementação única para o método que grava mensagens no console:
public static class LoggingMethods
{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}
Por fim, você precisa conectar o delegado anexando-o ao delegado WriteMessage declarado no agente:
Logger.WriteMessage += LoggingMethods.LogToConsole;
Práticas
Nosso exemplo até agora é bastante simples, mas ainda demonstra algumas das diretrizes importantes para projetos envolvendo delegados.
O uso dos tipos de delegado definidos na estrutura principal facilita o trabalho dos usuários com os delegados. Você não precisa definir novos tipos e os desenvolvedores que usam sua biblioteca não precisam aprender novos tipos de delegado especializados.
As interfaces usadas são as mais mínimas e flexíveis possíveis: para criar um novo logger de saída, você deve criar um método. Esse método pode ser um método estático ou um método de instância. Ele pode ter qualquer acesso.
Formatar saída
Vamos tornar essa primeira versão um pouco mais robusta e, em seguida, começar a criar outros mecanismos de log.
Em seguida, vamos adicionar alguns argumentos ao método para que sua LogMessage() classe de log crie mensagens mais estruturadas:
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);
}
}
Em seguida, vamos usar esse Severity argumento para filtrar as mensagens que são enviadas para a saída do log.
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áticas
Você adicionou novos recursos à infraestrutura de registro em log. Como o componente do registrador está fracamente acoplado a qualquer mecanismo de saída, esses novos recursos podem ser adicionados sem impactar nenhum dos códigos que implementam o delegado do registrador.
À medida que você continuar criando isso, verá mais exemplos de como esse acoplamento flexível permite maior flexibilidade na atualização de partes do site sem nenhuma alteração em outros locais. De fato, em um aplicativo maior, as classes de saída do agente podem estar em um assembly diferente e nem mesmo precisar ser recriadas.
Criar um segundo mecanismo de saída
O componente Log está progredindo bem. Vamos adicionar mais um mecanismo de saída que registra mensagens em um arquivo. Esse será um mecanismo de saída de um pouco mais envolvido. Será uma classe que encapsula as operações de arquivo e garante que o arquivo seja sempre fechado após cada gravação. Isso garante que todos os dados sejam liberados para o disco após a geração de cada mensagem.
Aqui está o registrador baseado em arquivo:
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.
}
}
}
Depois de ter criado esta classe, você pode instanciá-la e ela anexará seu método LogMessage ao componente Logger.
var file = new FileLogger("log.txt");
Estes dois não são mutuamente exclusivos. Você pode anexar ambos os métodos de log e gerar mensagens para o console e para um arquivo.
var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier
Posteriormente, mesmo no mesmo aplicativo, você pode remover um dos delegados sem nenhum outro problema para o sistema:
Logger.WriteMessage -= LoggingMethods.LogToConsole;
Práticas
Agora, você adicionou um segundo manipulador de saída para o subsistema de log. Este precisa de um pouco mais de infraestrutura para dar suporte correto ao sistema de arquivos. O delegado um método de instância. Também é um método privado. Não há necessidade de maior acessibilidade porque a infraestrutura delegada pode conectar os delegados.
Em segundo lugar, o design baseado em delegado habilita vários métodos de saída sem nenhum código extra. Você não precisa criar nenhuma infraestrutura adicional para dar suporte a vários métodos de saída. Eles simplesmente se tornam outro método na lista de invocação.
Dedique atenção especial ao código no método de saída do registro em log de arquivos. Ele é codificado para garantir que ele não gere exceções. Embora isso nem sempre seja estritamente necessário, muitas vezes é uma boa prática. Se qualquer um dos métodos delegados gerar uma exceção, os delegados restantes que estão na invocação não serão invocados.
Como última observação, o registrador de arquivos deve gerenciar seus recursos, abrindo e fechando o arquivo para cada mensagem de log. Você pode optar por manter o arquivo aberto e implementar IDisposable para fechar o arquivo quando for concluído.
Qualquer um dos métodos tem suas vantagens e desvantagens. Ambos criam um pouco mais de acoplamento entre as classes.
Nenhum código na Logger classe precisaria ser atualizado para dar suporte a qualquer um dos cenários.
Manipular delegados nulos
Por fim, vamos atualizar o método LogMessage para que ele seja robusto para esses casos quando nenhum mecanismo de saída for selecionado. A implementação atual gerará um NullReferenceException quando o WriteMessage delegado não tiver uma lista de invocação anexada.
Você pode preferir um design que continua de forma discreta quando não há métodos anexados. Isso é fácil usando o operador condicional nulo, combinado com o Delegate.Invoke() método:
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
O operador condicional nulo (?.) provoca um curto-circuito quando o operando esquerdo (WriteMessage nesse caso) é nulo, o que significa que nenhuma tentativa é feita para logar uma mensagem.
Você não encontrará o Invoke() método listado na documentação para System.Delegate ou System.MulticastDelegate. O compilador gera um método de tipo seguro Invoke para qualquer tipo de delegado declarado. Neste exemplo, isso significa que Invoke aceita um único argumento string e tem um tipo de retorno void.
Resumo das práticas
Você já viu o início de um componente de log que poderia ser expandido com outros gravadores e outros recursos. Com o uso de delegados no design, esses diferentes componentes ficam acoplados de maneira flexível. Isso oferece várias vantagens. É fácil criar novos mecanismos de saída e anexá-los ao sistema. Esses outros mecanismos só precisam de um método: o método que grava a mensagem de log. É um design resiliente quando novos recursos são adicionados. O contrato necessário para qualquer gravador é implementar um método. Esse método pode ser estático ou de instância. Pode ser público, privado ou qualquer outro acesso legal.
A classe de agente pode fazer vários aprimoramentos ou alterações sem introduzir alterações interruptivas. Como qualquer classe, você não pode modificar a API pública sem o risco de interromper alterações. Mas, como o acoplamento entre o agente e qualquer mecanismo de saída ocorre somente por meio do delegado, nenhum outro tipo (como interfaces ou classes base) é envolvido. O acoplamento é o menor possível.