Il result pattern offre un'alternativa efficace alla gestione tradizionale degli errori tramite eccezioni, migliorando la leggibilità e le prestazioni del programma. Questo pattern è particolarmente utile nei contesti in cui si desidera un controllo più dettagliato sulle operazioni riuscite e fallite, gestendo gli errori come parte integrante del flusso di dati piuttosto che come eccezioni.
Implementazione
Immaginiamo di avere un sistema in cui dobbiamo aggiornare delle attività TODO e vogliamo applicare il result pattern per gestire il flusso di successo e fallimento.
Questo è solo un esempio concettuale, consiglio di utilizzare una libreria pronta all'uso come:
public class Result<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public Error Error { get; }
protected Result(bool isSuccess, T value, Error error)
{
IsSuccess = isSuccess;
Value = value;
Error = error ?? Error.None;
}
public static Result<T> Success(T value) => new Result<T>(true, value, Error.None);
public static Result<T> Failure(Error error) => new Result<T>(false, default, error);
// Conversione implicita da T a Result<T> (successo)
public static implicit operator Result<T>(T value) => Success(value);
// Conversione implicita da Error a Result<T> (fallimento)
public static implicit operator Result<T>(Error error) => Failure(error);
}// Rappresentazione errore
public sealed record Error(string Code, string Description)
{
public static readonly Error None = new(string.Empty, string.Empty);
public override string ToString() => $"{Code}: {Description}";
}Ora, vediamo come possiamo utilizzare il result pattern, lo andremo a utilizzare accoppiato al Mediator pattern per gestire la logica di aggiornamento di un'attività TODO. Il Mediator è utile per separare la logica di business dall'infrastruttura, oltre che disaccoppiare le logiche scongiurando referenze circolari.
public class UpdateToDoCommand : IRequest<Result<ToDoItem>>
{
public int Id { get; set; }
public string NewTitle { get; set; }
public UpdateToDoCommand(int id, string newTitle)
{
Id = id;
NewTitle = newTitle;
}
}public class UpdateToDoCommandHandler : IRequestHandler<UpdateToDoCommand, Result<ToDoItem>>
{
private readonly IToDoRepository _repository;
public UpdateToDoCommandHandler(IToDoRepository repository)
{
_repository = repository;
}
public async Task<Result<ToDoItem>> Handle(UpdateToDoCommand request, CancellationToken cancellationToken)
{
var toDoItem = await _repository.GetByIdAsync(request.Id);
if (toDoItem == null)
{
//return Result<ToDoItem>.Failure(new Error("NOT_FOUND", "Item not found"));
return new Error("NOT_FOUND", "Item not found");
}
toDoItem.UpdateTitle(request.NewTitle);
await _repository.UpdateAsync(toDoItem);
return toDoItem;
}
} In questo handler gestiamo il flusso di successo e fallimento utilizzando il generico Result<T>. Se l'attività non viene trovata, restituiamo un fallimento con un messaggio di errore; altrimenti, aggiorniamo l'attività e restituiamo un risultato di successo.
Notare che non è necessario instanziare il tipo di ritorno tramite Result<ToDoItem>.Failure o Result<ToDoItem>.Success in quanto la conversione implicita vista in precedenza ci permette di ridurre la verbosità del codice.
Utilizzo
Consiglio di limitare l'uso del Result Pattern allo strato più vicino all'utente, in questo caso immaginiamo di richiamare il comando UpdateToDoCommand dal Controller:
// ...
public class ToDoController : ControllerBase
{
// ...
[HttpPost]
public async Task<ActionResult> JustDoIt()
{
var command = new UpdateToDoCommand(1, "Nuovo Titolo");
var result = await _mediator.Send(command);
if (result.IsSuccess)
{
return Ok(result.Value.Title);
}
else
{
return BadRequest(result.Error);
}
}
}Grazie al result pattern, non è necessario gestire eccezioni nel flusso principale del codice. Possiamo verificare facilmente se l'operazione è riuscita e agire di conseguenza, migliorando così la leggibilità e la manutenibilità del codice.
Quando Utilizzare il Result Pattern e quando le Exceptions
Il result pattern è ideale per gestire situazioni in cui ci si aspetta che un'operazione possa fallire come parte del normale flusso di lavoro. Ad esempio, login fallito causa password errata, il result pattern è la scelta migliore.
In questo caso, il fallimento non rappresenta un'eccezione imprevista, ma una possibilità comune che deve essere gestita esplicitamente, come abbiamo visto nell'implementazione con il comando UpdateToDoCommand.
D'altra parte, le eccezioni sono più appropriate per gestire scenari imprevisti o straordinari, come errori critici di sistema o problemi che non fanno parte del normale flusso di esecuzione. Ad esempio, se si verifica un errore di connessione al database durante l'aggiornamento, è opportuno lanciare un'eccezione. Questo tipo di errore non dovrebbe essere gestito come un normale risultato, ma piuttosto come un evento straordinario che richiede un'attenzione speciale, poiché interrompe il funzionamento previsto.
In sostanza
Gli errori sono per gli utenti, mentre le eccezioni sono per gli sviluppatori.


