giovedì 31 gennaio 2013

Extreme Design: una piccola provocazione (5° parte)

Concentriamo le nostre energie sulla parte per avere l'elenco dei movimenti eseguiti sul conto corrente. L'output che vogliamo ottenere è la lista di tutti i depositi e prelievi eseguiti con l'indicazione del giorno e dell'importo.

   public interface IMovementsResponse
   {
      void Return(IEnumerable<IMovementVisit> movements);
   }

Come per le altre operazioni dobbiamo sottoscrivere il contratto con IOperation e implementiamo IMovementsResponse per restituire l'elenco dei movimenti registrati in Account.

   public class Movements : IOperation, IMovementsResponseIMovementVisitor
   {
      private readonly IAccount account;
      private readonly IViewResponse view;

      public Movements(IAccount account, IViewResponse view)
      {
         this.account = account;
         this.view = view;
      }

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

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

      void IMovementsResponse.Return(IEnumerable<IMovementVisit> movements)
      {
         foreach (var movement in movements)
            movement.Accept(this);
      }

      void IMovementVisitor.Visit(WithdrawalMovement movement)
      {
         view.Response(string.Format("{0} - PRELIEVO per euro: {1}"
                                     movement.Date,
                                     movement.Value));
      }

      void IMovementVisitor.Visit(DepositMovement movement)
      {
         view.Response(string.Format("{0} - DEPOSITO per euro: {1}"
                                     movement.Date, 
                                     movement.Value));
      }
   }

Notiamo che dopo aver ricevuto la lista dei movimenti, per ogni movimento verifichiamo il tipo e notifichiamo all'operatore i dati del movimento. Per fare questo definiamo due tipologie di movimenti:

   public abstract class Movement
   {
      private readonly Amount amount;

      protected Movement(Amount amount)
      {
         this.amount = amount;
         Date = DateTime.Now;
      }

      public decimal Value { get { return amount.Value; } }
      public DateTime Date { get; private set; }
   }

   public class DepositMovement : Movement, IMovementVisit
   {
      public DepositMovement(Amount amount) : base(amount)
      {
      }

      void IMovementVisit.Accept(IMovementVisitor visitor)
      {
         visitor.Visit(this);
      }
   }

   public class WithdrawalMovement : Movement, IMovementVisit
   {
      public WithdrawalMovement(Amount amount) : base(amount)
      {
      }

      void IMovementVisit.Accept(IMovementVisitor visitor)
      {
         visitor.Visit(this);
      }
   }

Usiamo il pattern Visitor per capire quale tipo di movimento stiamo iterando e come deve essere notificato il suo stato.

Per ottenere questo risultato, ogni movimento deve implementare la seguente interfaccia

   public interface IMovementVisit
   {
      void Accept(IMovementVisitor visitor);
   }

Mentre per intercettare il tipo di movimento in fase di analisi, l'oggetto Movements implementa l'interfaccia:

   public interface IMovementVisitor
   {
      void Visit(WithdrawalMovement movement);
      void Visit(DepositMovement movement);
   }

Ora aggiorniamo l'oggetto Account per gestire i movimenti:

   public interface IAccount
   {
      void BalanceRequest(IBalanceResponse balance);
      void Deposit(Amount amount);
      void Withdrawal(Amount amount, IWithdrawalResponse withdrawal);
      void MovementsRequest(IMovementsResponse movements);
   }

   public class Account : IAccount
   {
      private Amount current;
      private readonly List<IMovementVisit> movements;

      public Account()
      {
         current = new Amount(0);
         movements = new List<IMovementVisit>();
      }

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

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

      public void Withdrawal(Amount amount, IWithdrawalResponse withdrawal)
      {
         if (current.Value >= amount.Value)
         {
            current = new Amount(current.Value - amount.Value);
            movements.Add(new WithdrawalMovement(amount));
            withdrawal.Completed();
            return;
         }
         withdrawal.CreditInsufficient();
      }

      public void MovementsRequest(IMovementsResponse movement)
      {
         movement.Return(movements);
      }
   }

Vediamo lo stack per capire meglio la gestione dei movimenti. Facciamo un deposito e poi un prelievo, al termine delle due operazioni richiediamo la lista dei movimenti:

   Movements.IMovementVisitor.Visit(WithdrawalMovement movement)
   WithdrawalMovement.IMovementVisit.Accept(IMovementVisitor visitor)
   Movements.IMovementVisitor.Visit(DepositMovement movement)
   DepositMovement.IMovementVisit.Accept(IMovementVisitor visitor)
   Movements.IMovementsResponse.Return(IEnumerable<IMovementVisit> movements)
   Account.MovementsRequest(IMovementsResponse movement)
   Movements.Execute()
   Operations.IOperationsInput.Return(string value)
   View.IViewOperations.Request(IOperationsInput operations)
   Operations.Display()

Come per i casi precedenti siamo stati abbastanza ligi nel rispettare le limitazioni che ci siamo prefissati, ma questa volta abbiamo fatto una violazione. Infatti l'aggiunta dei movimenti ha implicato la modifica del codice esistente.
Sono state inserite due righe di codice rispettivamente in:

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

   public void Withdrawal(Amount amount, IWithdrawalResponse withdrawal)
   {
      if (current.Value >= amount.Value)
      {
         current = new Amount(current.Value - amount.Value);
         movements.Add(new WithdrawalMovement(amount));
         withdrawal.Completed();
         return;
      }
      withdrawal.CreditInsufficient();
   }

Comportando la seguente violazione:

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

Ovvero abbiamo violato il principle:

Nel prossimo post vedremo come evitare questa violazione.

In conclusione: siamo riusciti a sviluppare una soluzione che rispettasse alcune regole per:
  • avere un buon design,
  • isolare le responsabilità,
  • permettere di aggiungere funzionalità progressivamente
  • non intaccare il codice esistente
  • ridurre i rami di if solo a logica di confronto (i.e. Account.Withdrawal)

Sono principi che dovrebbero essere sempre ricordati quando progettiamo del codice. Ovviamente questo comporta una forte disciplina e uno sforzo nella stesura del codice, ma la pulizia ottenuta ripaga gli sforzi.

Analizzando il risultato finale ci accorgeremo che per implementare il tutto abbiamo creato 15 interfacce e 11 classi, in un altro post cercheremo di implementare lo stesso risultato ma con un codice più "snello".

lunedì 28 gennaio 2013

Extreme Design: una piccola provocazione (4° parte)


Nel precedente post abbiamo implementato il deposito, affrontiamo in questo post l'aggiunta della funzionalità per prelevare denaro.

Prima di procedere dobbiamo fare una premessa, il prelievo ha delle analogie al deposito per l'interazione con l'operatore. Infatti dopo l'esecuzione di richiesta di prelievo, l'oggetto Withdrawal notifica all'operatore che vuole sapere qual è l'importo desiderato da prelevare.
Ovviamente il prelievo non potrà essere confermato se la richiesta è superiore all'importo disponibile, questa problematica ci obbliga a realizzare due tipologie diverse di notifiche.

Vediamo come realizzare il tutto:

   // interfaccia per richiedere all'operatore l'importo
   public interface IWithdrawalInput
   {
      void Return(string value);
   }

Withdrawal deve rispettare il contratto con IOperation per fare parte della liste di operazioni disponibile all'operatore, inoltre come per Deposit interagisce con la View (IViewWithdrawal) tramite IWithdrawalInput per richiedere l'importo da prelevare. Tramite la notifica Return viene letto e trasformato da stringa ad Amount l'importo che si vuole prelevare e inviato ad Account.

   public class Withdrawal : IOperation, IWithdrawalInput, IWithdrawalResponse
   {
      private readonly IAccount account;
      private readonly IViewWithdrawal view;

      public Withdrawal(IAccount account, IViewWithdrawal view)
      {
         this.account = account;
         this.view = view;
      }

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

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

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

      void IWithdrawalResponse.Completed()
      {
         view.Response(string.Format("Prelievo eseguito."));
      }

      void IWithdrawalResponse.InsufficientCredit()
      {
         view.Response(string.Format("Importo non disponibile."));
      }
   }

Vediamo come View notifica l'importo da prelevare:

   public interface IViewWithdrawal : IViewResponse
   {
      void Request(IWithdrawalInput withdrawal);
   }

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

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

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

      void IViewWithdrawal.Request(IWithdrawalInput withdrawal)
      {
         withdrawal.Return(Console.ReadLine());
      }
   }

Vediamo come Withdrawal collabora con Account.

   public interface IAccount
   {
      void BalanceRequest(IBalanceResponse balance);
      void Deposit(Amount amount);
      void Withdrawal(Amount amount, IWithdrawalResponse withdrawal);
   }

   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);
      }

      public void Withdrawal(Amount amount, IWithdrawalResponse withdrawal)
      {
         if (current.Value >= amount.Value)
         {
            current = new Amount(current.Value - amount.Value);
            withdrawal.Completed();
            return;
         }
         withdrawal.InsufficientCredit();
      }
   }

Quando Account riceve la richiesta di prelievo, oltre l'importo vuole sapere l'oggetto a cui notificare lo stato della richiesta:

   // contratto per notificare all'operatore se il prelievo 
   // è confermato o se il credito non è sufficiente
   public interface IWithdrawalResponse
   {
      void Completed();
      void InsufficientCredit();
   }

Analizziamo lo stack per vedere i due tipi di feedback, conseguenza della richiesta di prelievo:

Caso 1: prelievo superiore al credito disponibile

   Withdrawal.IWithdrawalResponse.CreditInsufficient()
   Account.Withdrawal(Amount amount, IWithdrawalResponse withdrawal)
   Withdrawal.IWithdrawalInput.Return(string value)
   View.IViewWithdrawal.Request(IWithdrawalInput withdrawal)
   Withdrawal.Execute()
   Operations.IOperationsInput.Return(string value)
   View.IViewOperations.Request(IOperationsInput operations)
   Operations.Display()

Caso 2: prelievo valido

   Withdrawal.IWithdrawalResponse.Completed()
   Account.Withdrawal(Amount amount, IWithdrawalResponse withdrawal)
   Withdrawal.IWithdrawalInput.Return(string value)
   View.IViewWithdrawal.Request(IWithdrawalInput withdrawal)
   Withdrawal.Execute()
   Operations.IOperationsInput.Return(string value)
   View.IViewOperations.Request(IOperationsInput operations)
   Operations.Display()

Anche per lo sviluppo di Withdrawal abbiamo:
  • tutti i metodi sono void
  • abbiamo un if per gestire il parse da stringa ad amount
  • abbiamo un if per verificare il prelievo richiesto
  • solo un metodo ha due parametri
  • abbiamo isolato le notifiche nello scope di competenza
  • tutto il codice può essere messo sotto test