lunedì 31 dicembre 2012

Considerazioni sui valori di ritorno (1° parte)

Quando progettate il vostro oggetto, pensate sempre che ci sarà qualcuno che lo utilizzerà al vostro posto. Quindi deve essere il vostro design a guidare l'utilizzo dell'oggetto.


Vi elenco una serie di accorgimenti da provare nei vostri prossimi sviluppi.


Non ritornate mai un null quando non sapete che valore restituire

   public object DoSomething() 
   { 
      if (IsOK()) 
         return new Object(); 
      return null; 
   }


Non ritornate mai un valore di default quando non sapete che valore restituire

   public int DoSomething() 
   { 
      if (IsOK()) 
         return 100; 
      return -1; 
   }



Non ritornate null quando dovete restituire una lista


   public object[] DoSomething()

   { 
      if (IsOK()) 
         return new[]{new object(),new object(),new object() }; 
      return null; 
   }



Non usare eccezioni se non sono realmente necessarie


   public object DoSomething() 
   { 
      if (IsOK()) 
         return new Object(); 
      throw new Exception();
   }



Per ovviare a queste scelte ci sono vari approcci.

Potete creare un oggetto che incapsula il valore di ritorno e che può eseguire operazioni se il valore di ritorno è valido.

Esempio:

tutti i casi precedenti si possono risolvere con una sintassi simile alla seguente

   public IDelegate<object> DoSomething() 
   { 
      if (IsOK()) 
         return new Delegate<object>(new object()); 
      return new NoDelegate<object>();
   }



Vediamo in pratica come si usano:



   public class Main : IPerform<object> 
   { 
      public Main() 
      { 
         DoSomething().Perform(this); 
      }


      public void Execute(object value) 
      { 
         Console.WriteLine(value); 
      } 
   }



Queste le implementazioni delle due tipologie di Delegate

   public interface IPerform<in T>
   { 
      void Execute(T value); 
   } 

   public interface IDelegate<out T>
   { 
      void Perform(IPerform<T> perform); 
   } 

   public class Delegate<T> : IDelegate<T>
   { 
      private readonly T value; 
      public Delegate(T value)
      { 
         this.value = value; 
      } 
      public void Perform(IPerform<T> perform)
      { 
         perform.Execute(value); 
      } 
   } 

   public class NoDelegate<T> : IDelegate<T>
   { 
      public void Perform(IPerform<T> perform) { } 
   }


Per renderla più snella, vi consiglio di ovviare la scelta dell'interfaccia IPerform ed usare una Action


   public class Main
   { 
      public Main() 
      { 
         DoSomething().Perform(value => Console.WriteLine(value)); 
      } 
   }



Quando le opzioni di ritorno ha più casi della semplice combo positivo/negativo, potete esporre tutte le opzioni possibili nell'interfaccia: 

   public IDelegate<object> DoSomething() 
   { 
      switch (GetStatus()) 
      { 
         case 1: 
            return new Delegate1<object>(new object()); 
         case 2: 
            return new Delegate2<object>(new object()); 
         case 3: 
            return new Delegate2<object>(new object()); 
      } 
      return new Delegate<object>(); 
   }

   public class Main 
   { 
      public Main() 
      { 
         var result = DoSomething(); 
         result.DoCase1(value => Console.WriteLine("case1:" + value)); 
         result.DoCase2(value => Console.WriteLine("case2:" + value)); 
         result.DoCase3(value => Console.WriteLine("case3:" + value)); 
      } 
   }



ecco una possibile implementazione:


   public interface IDelegate<out T> 
   { 
      void DoCase1(Action<T> perform); 
      void DoCase2(Action<T> perform); 
      void DoCase3(Action<T> perform); 
   }


   public class Delegate<T> : IDelegate<T> 
   { 
      public virtual void DoCase1(Action<T> perform) { } 
      public virtual void DoCase2(Action<T> perform) { } 
      public virtual void DoCase3(Action<T> perform) { } 
   }


   public class Delegate1<T> : Delegate<T> 
   { 
      private readonly T value; 
      public Delegate1(T value) { this.value = value; } 
      public override void DoCase1(Action<T> perform) { perform(value); } 
   }


   public class Delegate2<T> : Delegate<T> 
   { 
      private readonly T value; 
      public Delegate2(T value) { this.value = value; } 
      public override void DoCase2(Action<T> perform) { perform(value); } 
   }


   public class Delegate3<T> : Delegate<T> 
   { 
      private readonly T value; 
      public Delegate3(T value) { this.value = value; } 
      public override void DoCase3(Action<T> perform) { perform(value); } 
   }



Questa strada lascia aperta la possibilità all'utilizzatore di non dichiarare le callback da usare in tutti i casi. Quindi in questo caso potete implementare un builder per obbligare la definizione delle varie Action, oppure obbligare l'implementazione di una interfaccia con le tre callback. Vediamo queste due soluzioni nel prossimo post.