Ridurre il codice di gestione degli errori duplicato in C#?
-
08-06-2019 - |
Domanda
Non sono mai stato completamente soddisfatto del modo in cui funziona la gestione delle eccezioni, ci sono molte eccezioni e try/catch porta sul tavolo (svolgimento dello stack, ecc.), ma sembra rompere gran parte del modello OO nel processo.
Ad ogni modo, ecco il problema:
Supponiamo che tu abbia una classe che racchiude o include operazioni IO di file in rete (ad es.leggere e scrivere su qualche file in un particolare percorso UNC da qualche parte).Per vari motivi non vuoi che le operazioni di I/O falliscano, quindi se rilevi che falliscono, riprovale e continui a riprovarle finché non hanno esito positivo o raggiungi un timeout.Ho già una comoda classe RetryTimer che posso istanziare e utilizzare per sospendere il thread corrente tra i tentativi e determinare quando è trascorso il periodo di timeout, ecc.
Il problema è che hai un sacco di operazioni IO in diversi metodi di questa classe e devi racchiuderle ciascuna nella logica try-catch/retry.
Ecco uno snippet di codice di esempio:
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();
}
}
Quindi, come evitare di duplicare la maggior parte di questo codice per ogni operazione di I/O di file in tutta la classe?La mia soluzione era utilizzare blocchi delegati anonimi e un singolo metodo nella classe che eseguiva il blocco delegato che gli veniva passato.Questo mi ha permesso di fare cose come queste in altri metodi:
this.RetryFileIO( delegate()
{
// some code block
} );
Questo mi piace un po', ma lascia molto a desiderare.Mi piacerebbe sapere come altre persone potrebbero risolvere questo tipo di problema.
Soluzione
Sembra un'eccellente opportunità per dare un'occhiata alla programmazione orientata agli aspetti.Ecco un buon articolo su AOP in .NET.L'idea generale è che si estrarrebbero le preoccupazioni interfunzionali (ad es.Riprova per x ore) in una classe separata e quindi annoteresti tutti i metodi che devono modificare il loro comportamento in quel modo.Ecco come potrebbe apparire (con un bel metodo di estensione su Int32)
[RetryFor( 10.Hours() )]
public void DeleteArchive()
{
//.. code to just delete the archive
}
Altri suggerimenti
Mi chiedevo solo: cosa ritieni che il tuo metodo lasci a desiderare?Potresti sostituire il delegato anonimo con un...di nome?delegato, qualcosa del genere
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" );
}
Potresti anche utilizzare un approccio più OO:
- Crea una classe base che gestisca gli errori e chiami un metodo astratto per eseguire il lavoro concreto.(Modello metodo modello)
- Crea classi concrete per ogni operazione.
Ciò ha il vantaggio di dare un nome a ciascun tipo di operazione eseguita e di fornire uno schema di comando: le operazioni sono state rappresentate come oggetti.
Ecco cosa ho fatto di recente.Probabilmente è stato fatto meglio altrove, ma sembra abbastanza pulito e riutilizzabile.
Ho un metodo di utilità che assomiglia a questo:
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);
}
Quindi nella mia applicazione utilizzo la sintassi dell'espressione Lamda di C# per mantenere le cose in ordine:
Utility.DoAndRetry( () => ie.GoTo(url), 5);
Questo chiama il mio metodo e riprova fino a 5 volte.Al quinto tentativo, l'eccezione originale viene lanciata nuovamente all'interno di un'eccezione di nuovo tentativo.