Gli oggetti sono modelli. Non devono corrispondere agli oggetti del mondo reale. A volte le azioni devono essere modellate.
Prendi, ad esempio, lo scenario tipico del conto bancario, che viene utilizzato in molti corsi introduttivi di OO. Il design che viene insegnato assomiglia un po 'a questo:
class BankAccount {
Money balance;
void deposit(Money amount) {
balance += amount;
}
bool withdraw(Money amount) {
if (balance < amount) { return false; }
balance -= amount;
return true;
}
bool transfer(Money amount, BankAccount target) {
if (balance < amount) { return false; }
withdraw(amount);
target.deposit(amount);
return true;
}
}
Quindi, come puoi vedere, balance
è data e transfer
è un'operazione. Ha senso, vero?
Ma ci sono un paio di problemi. Ad esempio, se ho due conti bancari e voglio trasferire fondi da uno all'altro, ma entrambi hanno un metodo transfer
, perché è il metodo da chiamare con transfer
nell'account sorgente? Perché non quello nell'account di destinazione?
Possiamo modellarlo in modo diverso? Sì possiamo! In effetti, come viene fatto il banking "nel mondo reale"? Non ci sono soldi nel tuo account. E quando trasferisci fondi, non vengono prelevati soldi dal tuo account e inseriti in un altro account. Viene invece creato un fascicolo di transazione , che elenca l'account di origine, l'account di destinazione e l'importo. E alla fine della giornata, le polizze di transazione vengono tutte sommate e quindi sappiamo quanti soldi ci sono in ogni account.
Nota come abbiamo quasi doppiato il design ora: la quantità non è più dati, è un'operazione, e il trasferimento non è più un'operazione, sono dati:
class Transaction {
BankAccount source;
BankAccount target;
Money amount;
}
class BankAccount {
Money balance() {
// find all transaction slips which have 'this' as either source or target
// add all the amounts which have 'this' as target
// subtract all the amounts which have 'this' as source
return result;
}
void deposit(Money amount) {
TransactionLog.append(new Transaction(CASH, this, amount));
}
void withdraw(Money amount) {
TransactionLog.append(new Transaction(this, CASH, amount));
}
}
Questo modello presenta alcuni vantaggi. Nota: il fatto che sia più vicino a come funziona il banking "nel mondo reale" in realtà non è necessariamente un vantaggio. Dopotutto, stiamo costruendo un sistema software, non una banca, non siamo vincolati dalle realtà fisiche, quindi il sistema non deve rispecchiare il mondo reale.
Per uno: non c'è (quasi) nessuna mutazione e nessun effetto collaterale. Tutti gli oggetti sono immutabili e tutti i metodi sono referenzialmente trasparenti. (Sto sorvolando su come potrebbe essere implementato TransactionLog.append
, però). Aggiungere asincronia, concorrenza e parallelismo a questo sistema sarà più facile rispetto al primo. Il TransactionLog
lascia una traccia di controllo di tutte le transazioni. Questo è generalmente importante per il settore bancario. (Ad esempio, potresti aggiungere un ID utente dell'utente che ha avviato la transazione agli oggetti Transaction
.)
Non c'è anche asimmetria in transfer
. Non è più nemmeno un metodo del conto, ora sarà un metodo del sistema bancario.
Come puoi vedere, possiamo arrivare a due progetti completamente opposti per lo stesso identico problema. Nessuno di questi è giusto o sbagliato. Sono, come ho detto, modelli e se sono o meno "buoni" modelli dipendono dal contesto in cui verranno utilizzati e dalle proprietà importanti che dovrebbero modellare.
All'inizio della veeeeery di un corso OO, a volte viene insegnato il metodo di progettazione semplicemente scrivendo il caso d'uso e sottolineando tutti i verbi e i nomi. I nomi diventano oggetti, i verbi diventano metodi. E l'istruttore insegnerà spesso questo come se fosse un metodo di progettazione infallibile. Ma considera questo: in inglese, puoi pronunciare un verbo e anche il nome dei verbi è possibile. Quindi, dipende davvero da come scrivi il caso d'uso.