giovedì 24 gennaio 2013

Extreme Design: una piccola provocazione (3° parte)

Proviamo ad implementare la richiesta di deposito.

Dobbiamo sempre implementare l'interfaccia IOperation per poter interagire con IOperations. Notate che Deposit rispetto a Balance notifica le sue richieste non Direttamente a IView ma all'interfaccia IViewDeposit

   public interface IViewDeposit : IViewResponse
   {
      void Request(IDepositInput deposit);
   }

Questo avviene tramite la notifica Request che invia l'oggetto che richiede l'importo da depositare all'operatore, infatti Deposit passa se stesso a IViewDeposit.

Quando l'operatore inserisce l'importo Deposit riceve tramite la notifica IDepositInput.Return il valore da verificare prima di richiedere ad Account di depositare l'importo richiesto.

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

   public class Deposit : IOperation, IDepositInput
   {
      private readonly IAccount account;
      private readonly IViewDeposit view;

      public Deposit(IAccount account, IViewDeposit view)
      {
         this.account = account;
         this.view = view;
      }

      public void Display(Action<string> action)
      {
         action("Deposito");
      }

      public void Execute()
      {
         view.Response(string.Format("Inserire importo da depositare"));
         view.Request(this);
      }

      void IDepositInput.Return(string value)
      {
         decimal amount;
         if (decimal.TryParse(value, out amount))
         {
            account.Deposit(new Amount(amount));
            return;
         }
         view.Response(string.Format("Importo inserito non valido"));
         Execute();
      }
   }

Vediamo come aggiornare l'oggetto View per gestire questa nuova funzionalità:

   public class View : IViewOperations, IViewDeposit
   {
      void IViewResponse.Response(string output)
      {
         Console.WriteLine(output);
      }

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

      void IViewDeposit.Request(IDepositInput deposit)
      {
         deposit.Return(Console.ReadLine());
      }
   }

Analizziamo lo stack per capire il flusso delle richieste e notifiche:

   Account.Deposit(Amount amount)
   Deposit.IDepositInput.Return(string value)
   View.IViewDeposit.Request(IDepositInput deposit)
   Deposit.Execute()
   Operations.IOperationsInput.Return(string value)
   View.IViewOperations.Request(IOperationsInput operations)

Notate che:
  • tutti i metodi sono void
  • l'aggiunta delle nuove funzionalità non ha coinvolto il codice esistente, sono state solo aggiunte le parti per gestire la nuova funzionalità; Implica che tutti i test continuano a funzionare
  • le interazioni tra gli oggetti sono isolati nell'ambito di appartenenza, non è possibile chiamare implicitamente un metodo che non è attinente all'area di esecuzione
Aggiorniamo anche Account per gestire la richiesta di deposito:

   public interface IAccount
   {
      void BalanceRequest(IBalanceResponse balance);
      void Deposit(Amount amount);
   }

   public class Account : IAccount
   {
      private Amount current;

      public Account()
      {
         current = new Amount(0);
      }

      public void BalanceRequest(IBalanceResponse balance)
      {
         balance.Return(current);
      }

      public void Deposit(Amount amount)
      {
         current = new Amount(current.Value + amount.Value);
      }
   }

Note: l'unico if inserito è per il parse della stringa ad intero.

lunedì 21 gennaio 2013

Extreme Design: una piccola provocazione (2° parte)

Continuiamo l'esperimento, vediamo come implementare l'operazione per richiedere il saldo.
Tra le varie limitazioni che ci siamo imposti, dobbiamo rispettare :

   "l'aggiunta di una nuova operazione non deve essere invasiva"

per rispettare questa richiesta dobbiamo usare un contratto comune a tutte le operazioni che vogliamo implementare:

   public interface IOperation
   {
      void Display(Action<string> action);
      void Execute();
   }

   public interface IBalanceResponse
   {
      void Return(Amount amount);
   }

   public class Balance : IOperation, IBalanceResponse
   {
      private readonly IAccount account;
      private readonly IViewOperations view;

      public Balance(IAccount account, IViewOperations view)
      {
         this.account = account;
         this.view = view;
      }

      public void Display(Action<string> action)
      {
         action("Saldo");
      }

      public void Execute()
      {
         account.BalanceRequest(this);
      }

      void IBalanceResponse.Return(Amount amount)
      {
         view.Response(string.Format("Il saldo corrente corrisponde a euro: {0}", amount.Value));
      }
   }

L'oggetto Balance implementa IOperation che è il contratto comune per gestire tutte le operazioni sull'oggetto IAccount.

Dopo aver ricevuto l'input dalla View, l'oggetto Operations converte il valore nella corrispondente operazione.

Quando viene eseguito il comando Balance, viene chiamato BalanceRequest di Account passando l'oggetto che riceverà il feedback, a tal uopo Balance implementa IBalanceResponse per isolare lo scope di utilizzo.

Vediamo come è stato implementato IAccount:

   public interface IAccount
   {
      void BalanceRequest(IBalanceResponse balance);
   }

   public class Account : IAccount
   {
      private Amount current;

      public Account()
      {
         current = new Amount(0);
      }

      public void BalanceRequest(IBalanceResponse balance)
      {
         balance.Return(current);
      }
   }

Quando l'operatore richiede il saldo, Account riceve la richiesta e notifica tramite il messaggio Return il valore del saldo corrente. Anche in questo caso abbiamo usato un sistema di notifica per far interagire due oggetti.

Stiamo applicando i seguenti principle:

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 View : IViewOperations
   {
      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.