Utilizzando la tecnica precedente per eliminare la null reference exception, delegando l'oggetto Substitute ad eseguire le operazioni, qualcuno potrebbe dire: “ho perso la possibilità di eseguire un'operazione diversa se l'oggetto è null” (i.e. esempio tracciare che una determinata richiesta ha restituito null e voglio loggare oppure scatenare una eccezione). A prescindere dal tipo di operazione che vogliamo effettuare, l'affermazione precedente è vera; vediamo come possiamo introdurre questa possibilità mantenendo il codice pulito e leggibile, senza introdurre IF.
Vediamo
quale potrebbe essere il risultato finale:
new
Repository()
.StudentFoundByCode(new
CodeStudent
{Value = 1234})
.Perform(new
Output().Write)
.Fail(input
=> Console.WriteLine(input.Value))
.Perform<Worker>(new
Converter().Do)
.Fail(input
=> Console.WriteLine(input.Lastname))
.Perform(input
=> Console.WriteLine(input.ToString()));
In
questo modo ad ogni chiamata Perform su un oggetto, se l'oggetto è
Null possiamo eseguire una operazione alla chiamta Fail.
Per
ottenere questo tipo di sintassi dobbiamo creare due interfacce che
gestiscano rispettivamente la possibilità di eseguire operazioni e
di tracciare gli eventuali fallimenti causati da una istanza
valorizzata a null:
public
interface
ISubstitute<out
TI, out
T>
where
TI : class
where
T : class
{
IAlternative<TI,
T> Perform(Action<T>
action);
IAlternative<T,
TR> Perform<TR>(Func<T,
TR> action)
where
TR : class;
}
public
interface IAlternative<out
TI, out T>
where
TI : class
where
T : class
{
ISubstitute<TI,
T> Fail(Action<TI>
action);
}
Per
poter gestire la catena di operazioni, alternando azioni concrete ad
eventuali verifiche di fallimento, dobbiamo implementare tre classi che
derivano da entrambe le interfacce:
public
class Substitute<TI,
T> : ISubstitute<TI, T>,
IAlternative<TI, T>
where
TI : class
where
T : class
{
private
readonly
T instance;
public
Substitute(T instance)
{
this.instance
= instance;
}
//
esegue operazioni atomiche sullo stesso tipo di input
public
IAlternative<TI,
T> Perform(Action<T>
action)
{
action(instance);
return
this;
}
//
permette di trasformare l'input ricevuto in un altro tipo
public
IAlternative<T,
TR> Perform<TR>(Func<T,
TR> action)
where
TR : class
{
var
result = action(instance);
if
(result == null)
return
new
NullSubstitute<T,
TR>(instance);
return
new
Substitute<T,
TR>(result);
}
//
non esegue nessuna operazione perchè l'input non è null
public
ISubstitute<TI,
T> Fail(Action<TI>
action)
{
return
this;
}
}
Ora
vediamo come dobbiamo implementare la classe per gestire gli
oggetti null e come tracciare la loro presenza, fornendo in fase di
tracciatura l'input che ha generato l'oggetto null.
public
class NullSubstitute<TI,
T> : ISubstitute<TI, T>,
IAlternative<TI, T>
where
TI : class
where
T : class
{
private
readonly
TI input;
public
NullSubstitute(TI input)
{
this.input
= input;
}
//
esegue l'operazione per tracciare l'istanza null e termina la
// catena
tramite l'oggetto BlockSubstituteChain
public
ISubstitute<TI,
T> Fail(Action<TI>
action)
{
action(input);
return
new
BlockSubstituteChain<TI,
T>();
}
//
non esegue nessuna operazione perchè l'input è null
public
IAlternative<TI,
T> Perform(Action<T>
action)
{
return
this;
}
//
non esegue nessuna operazione perchè l'input è null, ma deve
// restiture un oggetto di differente, in tal caso essendo l'input
// non utilizzabile
istanziamo un'altro oggetto che ha il compito
// di terminare la catena di
operazioni
public
IAlternative<T,
TR> Perform<TR>(Func<T,
TR> action)
where
TR : class
{
return
new
BlockSubstituteChain<T,
TR>();
}
}
Implementiamo
l'oggetto per interompere la catena quando incappiamo in un oggetto
null.
public
class BlockSubstituteChain<TI,
T> : ISubstitute<TI, T>,
IAlternative<TI, T>
where
TI : class
where
T : class
{
//
non esegue nessuna operazione perchè l'input è null
public
IAlternative<TI,
T> Perform(Action<T>
action)
{
return
this;
}
//
non esegue nessuna operazione perchè l'input è null, ma deve
// restiture un oggetto di differente, in tal caso essendo l'input
// non utilizzabile
istanziamo un'altro oggetto che ha il compito
// di terminare la catena di
operazioni
public
IAlternative<T,
TR> Perform<TR>(Func<T,
TR> action)
where
TR : class
{
return
new
BlockSubstituteChain<T,
TR>();
}
//
non esegue nessuna operazione perchè l'input è null ed il
// tracciamento è stato effettuato nella class NullSubstitute
public
ISubstitute<TI,
T> Fail(Action<TI>
action)
{
return
this;
}
}
Definendo
queste classi e la loro interazione, potete ottenere la sintassi
proposta all'inizio del post, ovviamente è un' interpretazione di
come implementare un sistema che vi permette di evitare eccezioni non
previste, tracciarle ed eliminiare eventuali IF.
Da
notare:
- gli input per i metodi Perform e Fail sono dei delegate per disaccopiare il più possibile le classi che utilizzeranno questo sistema;
- i tipi generici sono stati impostati a classi per due motivi: permettere di verificare se l'istanza è null e perchè preferisco evitare l'uso dei value types (come spiegato in un altro post).
Nessun commento:
Posta un commento