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