venerdì 29 novembre 2013

dal TDD al BDD



Durante l'Agile Day 2013 ho tenuto una sessione che 

verte sull'adozione del BDD nello sviluppo/progettazione del software.


Il materiale della sessione "dal TDD al BDD":





Grazie per la partecipazione e il feedback.

lunedì 11 febbraio 2013

Extreme Design: una piccola provocazione (6° parte)

   "Aggiungere una nuova funzionalità non deve essere invasivo"

Come possiamo evitare di violare il seguente principio?

Dobbiamo rispettare un'altro principio:

   "Le operazioni da eseguire devono essere isolate nel contesto di utilizzo"

Avevamo definito il contratto di Account come segue:

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

Questo implica che Account deve gestire direttamente l'elenco dei movimenti, ma proviamo a separare le responsabilità isolando la funzionalità MovemensRequest in un'altra intefaccia:

   public interface IMovementsAccount
   {
      void MovementsRequest(IMovementsResponse movements);
   }

Ora Account è definito come segue:

   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.CreditInsufficient();
      }
   }

Siamo tornati alla versione precedente alla richiesta di gestire i movimenti.
Ora vediamo come progettare un oggetto per gestire i movimenti eseguiti sul conto corrente:

   public class MovementsAccount: Account, IMovementsAccount, IWithdrawalResponse
   {
      private readonly List<IMovementVisit> movements = new List<IMovementVisit>();
      private Amount amountWithdrawal;

      public override void Deposit(Amount amount)
      {
         base.Deposit(amount);
         movements.Add(new DepositMovement(amount));
      }

      public override void Withdrawal(Amount amount, IWithdrawalResponse withdrawal)
      {
         amountWithdrawal = amount;
         base.Withdrawal(amount, this);
      }

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

      void IWithdrawalResponse.Completed()
      {
         movements.Add(new WithdrawalMovement(amountWithdrawal));
      }

      void IWithdrawalResponse.CreditInsufficient()
      {
      }
}

In questa soluzione, ereditiamo dalla classe Account, facciamo l'override dei metodi Deposit e Withdrawal che sono coinvolti nella fase di registrazione dei movimenti.
Ovviamente abbiamo dovuto modificare la visibilità di Deposit e Withdrawal indicano che sono metodi virtuali. Quindi nella nostra progettazione di Account dovevamo avere una sfera di cristallo per prevedere che in futuro avremmo dovuto gestire i movimenti. Forse a questo punto è bene definire anche BalanceRequest come virtuale? Allora tutti i metodi dovrebbero essere virtuali?

Vediamo come abbiamo gestito la registrazione dei movimenti e poi analizziamo un'altra strada per evitare di creare i metodi virtuali.

Deposit risulta essere molto intuitivo, si deve solo occupare di registrare il movimento di deposito. Invece withdrawal ha una complicazione, registriamo il movimento solo se l'importo richiesto è inferiore al credito residuo. A questo punto creiamo un nuovo oggetto che decora le notifiche di IWithdrawalResponse e notifica all'oggetto richiedente il risultato del prelievo.

   public interface IWithdrawalResponseDispatcher : IWithdrawalResponse
   {
      void SetClients(params IWithdrawalResponse[] inners);
   }

   public class WithdrawalResponseDispatcherIWithdrawalResponseDispatcher
   {
      private IWithdrawalResponse[] inners = new IWithdrawalResponse[0];

      public void SetClients(params IWithdrawalResponse[] inners)
      {
         this.inners = inners;
      }

      void IWithdrawalResponse.Completed()
      {
         foreach (var inner in inners)
            inner.Completed();
      }

      void IWithdrawalResponse.CreditInsufficient()
      {
         foreach (var inner in inners)
            inner.CreditInsufficient();
      }
   }

Notate che l'oggetto non è altro che un Observer per le notifiche.


Aggiorniamo MovementsAccount:

   public class MovementsAccount : Account, IMovementsAccount, IWithdrawalResponse
   {
      private readonly IWithdrawalResponseDispatcher withdrawalResponseDispatcher;
      private readonly List<IMovementVisit> movements = new List<IMovementVisit>();
      private Amount amountWithdrawal;

      public MovementsAccount(IWithdrawalResponseDispatcher withdrawalResponseDispatcher)
      {
         this.withdrawalResponseDispatcher = withdrawalResponseDispatcher;
      }

      public override void Deposit(Amount amount)
      {
         base.Deposit(amount);
         movements.Add(new DepositMovement(amount));
      }

      public override void Withdrawal(Amount amount, IWithdrawalResponse withdrawal)
      {
         amountWithdrawal = amount;
         withdrawalResponseDispatcher.SetClients(this, withdrawal);
         base.Withdrawal(amount, withdrawalResponseDispatcher);
      }

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

      void IWithdrawalResponse.Completed()
      {
         movements.Add(new WithdrawalMovement(amountWithdrawal));
      }

      void IWithdrawalResponse.CreditInsufficient()
      {
      }
   }


A questo punto per ovviare i metodi virtuali potremmo seguire la stessa procedura e creare una classe che decora Account e che gestisca i movimenti:

   public class MovementsAccount : IAccount, IMovementsAccount
   {
      private readonly IAccount inner;
      private readonly  IWithdrawalResponseDispatcher withdrawalResponseDispatcher;
      private readonly List<IMovementVisit> movements;
      private Amount amountWithdrawal;

      public MovementsAccount(IAccount inner, IWithdrawalResponseDispatcher withdrawalResponseDispatcher)
      {
         this.inner = inner;
         this.withdrawalResponseDispatcher withdrawalResponseDispatcher;
         movements = new List<IMovementVisit>();
      }

      public void BalanceRequest(IBalanceResponse balance)
      {
         inner.BalanceRequest(balance);
      }

      public void Deposit(Amount amount)
      {
         inner.Deposit(amount);
         movements.Add(new DepositMovement(amount));
      }

      public void Withdrawal(Amount amount, IWithdrawalResponse withdrawal)
      {
         amountWithdrawal = amount;
         withdrawalResponseDispatcher.SetClients(this, withdrawal);
         inner.Withdrawal(amount, withdrawalResponseDispatcher);
      }

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

In questo modo MovementsAccount contiene un' istanza di Account con la quale interagisce senza la necessità di modificare i metodi di Account creandoli virtuali.

Ora l'oggetto Movements che gestiva la richiesta dei movimenti deve interagire con IMovementsAccount, questo è corretto visto che volevamo isolare le operazioni in base al contesto e alla responsabilità.

   public class Movements : IOperation, IMovementsResponse, IMovementVisitor
   {
      private readonly IMovementsAccount account;
      private readonly IViewResponse view;

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

Per finire l'unica modifica “invasiva” che dobbiamo fare è cambiare l'inizializzazione delle istanze di Account.

Prima di isolare le responsabilità:

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

Dopo la modifica con la decorazione:

   private static void Main()
   {
      var account = new MovementsAccount(new Account(), new  WithdrawalResponseDispatcher());
      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));
   }

Oppure usando l'ereditarietà:

   private static void Main()
   {
      var account = new MovementsAccount(new  WithdrawalResponseDispatcher());
      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));
   }

Non è molto importante disquisire se in questo caso è meglio usare l'ereditarietà o la composizione per progettare l'oggetto MovementsAccount.
Il punto cardine rimane: possiamo fare sviluppi incrementali non invasivi nel nostro codice se e solo se isoliamo le responsabilità e le interazioni tra gli oggetti.

Questo è possibile se, mentre sviluppiamo, ricordiamo i seguenti principles: