Reduzindo código duplicado de tratamento de erros em C#?
-
08-06-2019 - |
Pergunta
Nunca fiquei completamente satisfeito com a forma como o tratamento de exceções funciona, há muitas exceções e o try/catch traz para a mesa (desenrolamento da pilha, etc.), mas parece quebrar muito o modelo OO no processo.
De qualquer forma, aqui está o problema:
Digamos que você tenha alguma classe que agrupa ou inclui operações de E/S de arquivos em rede (por exemplo,lendo e gravando em algum arquivo em algum caminho UNC específico em algum lugar).Por vários motivos, você não deseja que essas operações de IO falhem; portanto, se você detectar que elas falham, tente-as novamente e continue tentando-as novamente até que sejam bem-sucedidas ou atinja o tempo limite.Já tenho uma classe RetryTimer conveniente que posso instanciar e usar para suspender o thread atual entre novas tentativas e determinar quando o período de tempo limite expirou, etc.
O problema é que você tem várias operações de IO em vários métodos dessa classe e precisa agrupar cada uma delas na lógica try-catch/retry.
Aqui está um exemplo de trecho de código:
RetryTimer fileIORetryTimer = new RetryTimer(TimeSpan.FromHours(10));
bool success = false;
while (!success)
{
try
{
// do some file IO which may succeed or fail
success = true;
}
catch (IOException e)
{
if (fileIORetryTimer.HasExceededRetryTimeout)
{
throw e;
}
fileIORetryTimer.SleepUntilNextRetry();
}
}
Então, como evitar a duplicação da maior parte desse código para cada operação de E/S de arquivo em toda a classe?Minha solução foi usar blocos delegados anônimos e um único método na classe que executou o bloco delegado passado para ele.Isso me permitiu fazer coisas assim em outros métodos:
this.RetryFileIO( delegate()
{
// some code block
} );
Gosto um pouco disso, mas deixa muito a desejar.Eu gostaria de saber como outras pessoas resolveriam esse tipo de problema.
Solução
Esta parece ser uma excelente oportunidade para dar uma olhada na Programação Orientada a Aspectos.Aqui está um bom artigo sobre AOP em .NET.A ideia geral é que você extraia a preocupação multifuncional (ou seja,Tente novamente por x horas) em uma classe separada e então você anotaria quaisquer métodos que precisassem modificar seu comportamento dessa maneira.Veja como pode parecer (com um bom método de extensão no Int32)
[RetryFor( 10.Hours() )]
public void DeleteArchive()
{
//.. code to just delete the archive
}
Outras dicas
Só me perguntando: o que você acha que seu método deixa a desejar?Você poderia substituir o delegado anônimo por um ..nomeado?delegado, algo como
public delegate void IoOperation(params string[] parameters);
public void FileDeleteOperation(params string[] fileName)
{
File.Delete(fileName[0]);
}
public void FileCopyOperation(params string[] fileNames)
{
File.Copy(fileNames[0], fileNames[1]);
}
public void RetryFileIO(IoOperation operation, params string[] parameters)
{
RetryTimer fileIORetryTimer = new RetryTimer(TimeSpan.FromHours(10));
bool success = false;
while (!success)
{
try
{
operation(parameters);
success = true;
}
catch (IOException e)
{
if (fileIORetryTimer.HasExceededRetryTimeout)
{
throw;
}
fileIORetryTimer.SleepUntilNextRetry();
}
}
}
public void Foo()
{
this.RetryFileIO(FileDeleteOperation, "L:\file.to.delete" );
this.RetryFileIO(FileCopyOperation, "L:\file.to.copy.source", "L:\file.to.copy.destination" );
}
Você também pode usar uma abordagem mais OO:
- Crie uma classe base que faça o tratamento de erros e chame um método abstrato para realizar o trabalho concreto.(Padrão de método de modelo)
- Crie classes concretas para cada operação.
Isto tem a vantagem de nomear cada tipo de operação executada e fornece um padrão de comando - as operações foram representadas como objetos.
Aqui está o que fiz recentemente.Provavelmente já foi feito melhor em outro lugar, mas parece bastante limpo e reutilizável.
Eu tenho um método utilitário parecido com este:
public delegate void WorkMethod();
static public void DoAndRetry(WorkMethod wm, int maxRetries)
{
int curRetries = 0;
do
{
try
{
wm.Invoke();
return;
}
catch (Exception e)
{
curRetries++;
if (curRetries > maxRetries)
{
throw new Exception("Maximum retries reached", e);
}
}
} while (true);
}
Então, em meu aplicativo, uso a sintaxe de expressão Lamda do c# para manter as coisas organizadas:
Utility.DoAndRetry( () => ie.GoTo(url), 5);
Isso chama meu método e tenta novamente até 5 vezes.Na quinta tentativa, a exceção original é lançada novamente dentro de uma exceção de nova tentativa.