martedì 25 settembre 2012

Come evitare NullReferenceException (Evolution)

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