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".

Nessun commento:

Posta un commento