giovedì 27 dicembre 2012

Progettare oggetti, non scrivere solo codice

Quando un committente ci incarica di realizzare una nuova feature, normalmente ci concentriamo solo sul realizzo della richiesta senza valutare quale potrebbe essere il design per fornire maggiori vantaggi nel futuro. Lo sviluppo e il design dovrebbero essere due fasi che evolvono parallelamente e non una figlia dell'altra. Tutte le volte che scrivete del codice, dovreste pensare “ma un giorno, chi leggerà questo codice sarà in grado di capire come lo deve usare?”. Vi faccio un esempio molto semplice.

Richiesta del product owner: “trasferire dei soldi da un conto corrente ad un altro conto corrente.” 

La richiesta non è particolarmente complicata e abbiamo la fortuna che un nostro collega ha già creato l'oggetto Account per gestire il conto e noi dobbiamo solo occuparci del trasferimento.



Creiamo l'oggetto Transfer che si occuperà di trasferire il denaro da un conto corrente all'altro.

   public class Transfer
   {

      public void Transfer(Account from, Account to, Amount amount) 
      {
         from.Withdraw(amount); 
         to.Deposit(amount); 
      } 
   }

proviamo ad usarla:

   public class Main 
   {
      static Main() 
      { 
         var accountA = new Account(); 
         var accountB = new Account(); 
         new Transfer()
            .Transfer(accountA,accountB,new Amount(10)); 
      } 
   }

purtroppo durante l'utilizzo otteniamo una bella eccezione. A questo punto vediamo il sorgente di account per capire qual è stato il nostro errore.


   public class Account 
   { 
      Amount balance = new Amount(0); 
      
      public Amount GetBalance()
      { 
          return balance; 
      }


      public void Withdraw(Amount amount) 
      { 
          if (amount.Value <= balance.Value) 
          { 
              balance = new Amount(balance.Value - amount.Value); 
              return; 
          } 
          throw new Exception("Insufficient Credit"); 
      }


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


Guardando il codice, notiamo che se il credito del conto non è sufficiente viene scatenata una eccezione per indicare che l'operazione richiesta non è consentita. Purtroppo dal design di Account non traspare quale sarà il suo comportamento se il credito non è sufficiente. Quindi solo la documentazione può spiegare questo comportamento.



In alternativa, potremmo restituire un valore per indicare se l'operazione di Withdraw è andata a buon fine.

   public class Account
   { 
      public bool Withdraw(Amount amount) 
      { 
         if (amount.Value <= balance.Value) 
         { 
             balance = new Amount(balance.Value - amount.Value); 
             return true; 
         } 
         return false; 
      } 
   }

   public class Transfer 
   { 
      public void Transfer(Account from, Account to, Amount amount) 
      { 
         if (from.Withdraw(amount)) 
            to.Deposit(amount); 
      } 
   }

In questo caso l'utilizzo di withdraw non genera eccezioni, ma ancora una volta solo la documentazione ci permette di capire il significato del valore di ritorno, true è andato tutto bene e false abbiamo avuto qualche problema con il deposito.

Vi propongo un'altra strada, tramite gli eventi. In questo caso, account espone due eventi:

   public class Account 
   { 
      public event Action<Amount> WithdrawalGranted; 
      public event Action InsufficientCredit;


      public void Withdraw(Amount amount) 
      { 
         if (amount.Value <= balance.Value) 
         { 
            balance = new Amount(balance.Value - amount.Value); 
            if (WithdrawalGranted != null) 
               WithdrawalGranted(amount); 
         } 
         if (InsufficientCredit != null) 
            InsufficientCredit(); 
      } 
   }

E Transfer dovrà sottoscriversi agli eventi:


   public class Transfer 
   { 
      private Account toAccount; 
      
      public void Transfer(Account from, Account to, Amount amount)
      { 
         from.Withdraw(amount); 
      } 
   }


In questo caso, la struttura dell'oggetto ci espone due eventi, ma solo la documentazione o andando per tentativi possiamo capire come usare Account nel modo corretto e quindi produrre il seguente codice:



   public class Transfer 
   { 
      private Account toAccount; 

      public void Transfer(Account from, Account to, Amount amount)
      { 
         from.Withdraw(amount); 
         from.WithdrawalGranted += FromOnWithdrawalGranted; 
         toAccount = to; 
      }


      private void FromOnWithdrawalGranted(Amount amount) 
      { 
         toAccount.Deposit(amount); 
      } 
   }


Come potete notare il codice precedente presenta un bug. Quindi l'attuale design non ci permette di capire come utilizzare al meglio questo oggetto, senza inciampare in qualche errore banale.

Ora modifichiamo account per rendere più "usabile" questo codice.


   public class Account 
   { 
      public void Withdraw(IWithdrawNotifications notify, Amount money) 
      { 
         if (money.Value <= balance.Value) 
         { 
            balance = new Amount(balance.Value - money.Value); 
            notify.WithdrawalGranted(money); 
            return; 
         } 
         notify.InsufficientCredit(); 
      } 
   }


Obblighiamo chi utilizza Withdraw ad indicare l'oggetto sul quale vogliamo notificare il comportamento di Account. Quindi Tranfer diventa:

   public class Transfer : IWithdrawNotifications 
   { 
      private Account toAccount;


      public void Transfer(Account from, Account to, Amount amount) 
      { 
         from.Withdraw(this, amount); 
         toAccount = to; 
      } 

      public void WithdrawalGranted(Amount amount)
      { 
         toAccount.Deposit(amount); 
      }


      public void InsufficientCredit() 
      { 
      }
   } 

   public interface IWithdrawNotifications 
   { 
      void WithdrawalGranted(Amount amount); 
      void InsufficientCredit(); 
   }


Ora l'oggetto account, oltre ad eseguire il suo compito, spiega come interagisce con gli oggetti che la utilizzano. Però non abbiamo ancora finito, c'è sempre un bel bug da risolvere. Infatti, l'attuale implementazione di Transfer genera una bella null reference exception. Per ovviare a questo problema, concentriamo le nostre energie per rendere l'oggetto Transfer "usabile" come Account.

   public class Transfer : ITransferFrom, 
                           ITransferTo, 
                           ITransferAmount, 
                           IWithdrawNotifications 
   { 
      private Account fromAccount; 
      private Account toAccount;


      public ITransferTo From(Account account)
      { 
         fromAccount = account; 
         return this; 
      }

      ITransferAmount ITransferTo.To(Account account) 
      { 
         toAccount = account;
         return this; 
      }


      void ITransferAmount.For(Amount amount) 
      { 
         fromAccount.Withdraw(this, amount); 
      } 

      void IWithdrawNotifications.WithdrawalGranted(Amount amount)
      { 
         toAccount.Deposit(amount); 
      }

      void IWithdrawNotifications.InsufficientCredit() 
      { 
      } 
   }

   public interface ITransferFrom 
   { 
      ITransferTo From(Account account); 
   }

   public interface ITransferTo 
   { 
      ITransferAmount To(Account account); 
   }

   public interface ITransferAmount 
   { 
      void For(Amount amount);
   }




Vediamo come utilizzarla:


   public class Main 
   { 
      static Main() 
      { 
         var accountA = new Account(); 
         var accountB = new Account(); 
         new Transfer() 
            .From(accountA) 
            .To(accountB) 
            .For(new Amount(10)); 
      } 
   }


con questa implementazione, Account e Transfer rispondo all'esigenza per cui sono stati creati, ma hanno anche un design che li rende “usabili” senza avere una documentazione che illustra il comportamento.

Quando progettate i vostri oggetti, pensate sempre come devono interagire e come saranno utilizzati dai vostri colleghi. Questo vi aiuterà a progettare oggetti più semplici nell'utilizzo e vi permetterà di prevenire bug banali.

Se invece preferite, potete decorare i vostri oggetti con documentazione che illustri: l'utilizzo dell'oggetto, come evitare bug ed esplosioni inaspettate (notate l'espressione ironica sul mio volto?).

Nessun commento:

Posta un commento