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
WithdrawalResponseDispatcher
: IWithdrawalResponseDispatcher
{
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;
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;
inner.Withdrawal(amount, withdrawalResponseDispatcher);
withdrawalResponseDispatcher.SetClients(this, withdrawal);
}
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:
Nessun commento:
Posta un commento