venerdì 18 gennaio 2013

Extreme Design: una piccola provocazione (1° parte)

Gli oggetti dovrebbero interagire tramite messaggi: normalmente per avere un feedback da un oggetto, si scrive una funzione che restituisce un valore. Per ovviare a questa strada, facciamo una analogia con il meccanismo di comunicazione che avviene tra i neuroni. Quando due neuroni vogliono comunicare si collegano tramite una giunzione (sinapsi), in questo caso il neurone A invia un impulso chimico/elettrico al neurone B.

Proviamo a seguire lo stesso schema e cerchiamo di sviluppare una soluzione per gestire il conto corrente di un ipotetico cliente di una banca. L'idea è che un operatore allo sportello potrà fare le operazioni di: saldo, lista movimenti, prelievo, deposito. Per rendere la cosa più divertente dobbiamo seguire le seguenti regole:
  • tutti i metodi sono void
  • il minor numero di parametri per metodo
  • meno if possibili
  • tutto il codice deve poter essere messo sotto test
  • l'aggiunta di una nuova operazione non deve essere invasiva
  • le operazioni da eseguire devono essere isolate nel contesto di utilizzo
  • minima dipendenza tra oggetti
Dobbiamo realizzare una console application che riceve in input le operazioni da svolgere e deve restituire il feedback per l'operazione richiesta.

Il risultato finale potrebbe assomigliare a:

   public class Program
   {
      public Program(IOperations operations)
      {
         operations.Display();
      }

      private static void Main()
      {
         var account = new Account();
         var view = new View();
         var operations = new IOperation[]
            {
               new Balance(account, view),
               new Deposit(account, view),
               new Withdrawal(account,view),
               new Movements(account,view)
            };
         new Program(new Operations(view, operations));
      }
   }

vediamo come realizzare l'oggetto Operations:

   public class Operations : IOperations, IOperationsInput
   {
      private readonly IViewOperations view;
      private readonly IOperation[] operations;

      public Operations(IViewOperations view, IOperation[] operations)
      {
         this.view = view;
         this.operations = operations;
      }

      public void Display()
      {
         var index = 0;
         foreach (var operation in operations)
            operation.Display(value => 
               view.Response(string.Format("[{0}] - {1}", ++index, value)));
         view.Response("Seleziona l'operazione da eseguire:");
         view.Request(this);
      }

      void IOperationsInput.Return(string value)
      {
         if (value.ToLower() == "q")
            return;
         int index;
         if (int.TryParse(value, out index) && IsAvailableOperation(index))
         {
            operations[index - 1].Execute();
            Display();
            return;
         }
         view.Response("Operazione selezionata non valida");
         Display();
      }

      private bool IsAvailableOperation(int index)
      {
         return index > 0 && index <= operations.Length;
      }
   }

Operation interagisce con due tipologie di oggetti: IViewOperations e IOperation.

IViewOperations ha la responsabilità di interagire con la fonte dati, nel nostro caso la console utilizzata dall'operatore per gestire le operazioni sul conto corrente.

IOperation ha la responsabilità di eseguire l'operazione sul conto corrente.

Vediamo come è stata implementata la richiesta di selezionare una operazione.

   public interface IViewResponse
   {
      void Response(string output);
   }

   public interface IViewOperations: IViewResponse
   {
      void Request(IOperationsInput operations);
   }

   public class ViewIViewOperations
   {
      void IViewResponse.Response(string output)
      {
         Console.WriteLine(output);
      }

      void  IViewOperations.OperationRequest(IOperationsInput operations)
      {
         operations.Return(Console.ReadLine());
      }
   }

View ha tutti i metodi void e solo un parametro in ingresso. Notate come sono state isolate le varie operazioni tramite interfacce specifiche, questo comporta che OperationRequest è disponibile solo quando View è trattato come IViewOperations. Per evitare di restituire un valore di ritorno mentre chiamiamo OperationRequest, passiamo un oggetto che implementa IOperationsResponse:

   public interface IOperationsResponse
   {
      void Return(string value);
   }

Abbiamo isolato le operazioni disponibili nel cotesto di utilizzo, l'oggetto Operations implementa in modo esplicito IOperationsResponse, rendendo disponibile il metodo Return solo quando viene usato da IView.OperationRequest.

Analizziamo lo stack:

Operations.IOperationsInput.Return(string value)
View.IViewOperations.Request(IOperationsInput operations)
Operations.Display()

Come vedete Operations richiede alla View quale operazione eseguire la View invia all'oggetto Operations qual è stata l'operazione selezionata dall'utente.
View e Operations stanno interagendo tramite messaggi isolando le responsabilità nei metodi di competenza. Tutti i metodi possono essere messi direttamente sotto test.

mercoledì 2 gennaio 2013

Considerazioni sui valori di ritorno (2° parte)


Come promesso, vi presento le due implementazioni suggerite nel precedente post. Iniziamo con la soluzione battezzata “interfaccia con tre callback”.

Il metodo da chiamare ha un parametro che espone il contratto per interagire con i dati che elabora:

   public void DoSomething(IDelegate<object> @delegate)
   {
      switch (GetStatus())
      {
         case 1:
            @delegate.DoCase1(new object());
            break;
         case 2:
            @delegate.DoCase2(new object());
            break;
         case 3:
            @delegate.DoCase3(new object());
            break;
      }
   }

Quindi, l'utilizzatore del metodo DoSomething deve passare un oggetto che rispetta questo contratto:

   public interface IDelegate<in T>
   {
      void DoCase1(T value);
      void DoCase2(T value);
      void DoCase3(T value);
   }

Vediamo nella pratica cosa vuol dire:

   public class Main : IDelegate<object>
   {
      public Main()
      {
         DoSomething(this);
      }

      public void DoCase1(object value)
      {
         Console.WriteLine("case1: " + value);
      }

      public void DoCase2(object value)
      {
         Console.WriteLine("case2: " + value);
      }

      public void DoCase3(object value)
      {
         Console.WriteLine("case3: " + value);
      }
   }

La classe Main che utilizza DoSomething implementa l'interfaccia IDelegate<T> e passa se stessa per poter ricevere le informazioni in base al comportamento di DoSomething.

Questa soluzione è abbastanza concisa, purtroppo implica che la callback ci porta in un altro scope con tutto quello che ne consegue.

Vediamo come l'implementazione della seconda soluzione proposta (ovvero il builder) ci permette di ovviare al problema indicato sopra e di mantenere un codice più snello:

   public class Main
   {
      static Main()
      {
         DoSomething()
            .SetCase1(value => Console.WriteLine("case1: " + value))
            .SetCase2(value => Console.WriteLine("case2: " + value))
            .SetCase3(value => Console.WriteLine("case3: " + value))
            .Execute();
      }
   }

Come vediamo dall'esempio sopra, la sintassi è più compatta e leggibile, mantenendo flessibilità e rispettando l'obbligo di guidare l'utilizzatore.

Per fare tutto questo c'è uno sforzo maggiore da seguire.

Dobbiamo definire quattro interfacce che esprimono il comportamento del builder:

   public interface IDelegate1<out T>
   {
      IDelegate2<T> SetCase1(Action<T> action);
   }

   public interface IDelegate2<out T>
   {
      IDelegate3<T> SetCase2(Action<T> action);
   }

   public interface IDelegate3<out T>
   {
      IExecute SetCase3(Action<T> action);
   }

   public interface IExecute
   {
      void Execute();
   }

Nella classe Foo implementiamo le interfacce:

   public class FooIDelegate1<object>,
                     IDelegate2<object>,
                     IDelegate3<object>,
                     IExecute
   {
      private Action<object> action1;
      private Action<object> action2;
      private Action<object> action3;

      IDelegate2<object> IDelegate1<object>.SetCase1(Action<object> action)
      {
         action1 = action;
         return this;
      }

      IDelegate3<object> IDelegate2<object>.SetCase2(Action<object> action)
      {
         action2 = action;
         return this;
      }

      IExecute IDelegate3<object>.SetCase3(Action<object> action)
      {
         action3 = action;
         return this;
      }

      void IExecute.Execute()
      {
         switch (GetStatus())
         {
            case 1:
               action1(new object());
               break;
            case 2:
               action2(new object());
               break;
            case 3:
               action3(new object());
               break;
         }
      }
   }

Il risultato finale, a mio parere, vale lo sforzo fatto. In definitiva si riduce alla definizione di qualche interfaccia per obbligare l'utilizzatore ad impostare le callback da usare.