Modello di denaro il modo giusto per rappresentare coppie di unità di valore

The Money Pattern, definito da Martin Fowler e pubblicato in Patterns of Enterprise Application Architecture, è un ottimo modo per rappresentare coppie value-unit. Si chiama Money Pattern perché è emerso in un contesto finanziario e illustreremo il suo utilizzo principalmente in questo contesto usando PHP.


Un conto simile a PayPal

Non ho idea di come PayPal sia implementato, ma penso che sia una buona idea prendere la sua funzionalità come esempio. Lascia che ti mostri cosa intendo, il mio conto PayPal ha due valute: dollari USA ed euro. Mantiene separati i due valori, ma posso ricevere denaro in qualsiasi valuta, posso vedere il mio ammontare totale in una delle due valute e posso estrarre in uno qualsiasi dei due. Per questo esempio, immaginiamo di estrarre in una qualsiasi delle valute e la conversione automatica viene eseguita se il saldo di quella valuta specifica è inferiore a quello che vogliamo trasferire, eppure c'è ancora abbastanza denaro nell'altra valuta. Inoltre, limiteremo l'esempio a solo due valute.


Ottenere un account

Se dovessi creare e utilizzare un oggetto Account, vorrei inizializzarlo con un numero di account.

function testItCanCrateANewAccount () $ this-> assertInstanceOf ("Account", new Account (123)); 

Questo ovviamente fallirà perché non abbiamo ancora una classe Account.

account di classe 

Bene, scrivendolo in un nuovo "Account.php" file e richiedendolo nel test, lo ha fatto passare. Tuttavia, tutto ciò viene fatto solo per farci sentire a nostro agio con l'idea. Quindi, sto pensando di ottenere l'account id.

function testItCanCrateANewAccountWithId () $ this-> assertEquals (123, (new Account (123)) -> getId ()); 

In realtà ho cambiato il test precedente in questo. Non c'è motivo di mantenere il primo. Ha vissuto la sua vita, il che significa che mi ha costretto a pensare al account classe e effettivamente lo crea. Ora possiamo andare avanti.

account di classe private $ id; function __construct ($ id) $ this-> id = $ id;  public function getId () return $ this-> id; 

Il test sta passando e account sta iniziando a sembrare una vera classe.


valute

Sulla base della nostra analogia con PayPal, potremmo voler definire una valuta primaria e una secondaria per il nostro account.

conto $ privato; funzione protetta setUp () $ this-> account = new Account (123);  [...] function testItCanHavePrimaryAndSecondaryCurrencies () $ this-> account-> setPrimaryCurrency ("EUR"); $ This-> Account-> setSecondaryCurrency ( 'USD'); $ this-> assertEquals (array ('primary' => 'EUR', 'secondary' => 'USD'), $ this-> account-> getCurrencies ()); 

Ora il test sopra ci costringerà a scrivere il seguente codice.

account di classe private $ id; private $ primaryCurrency; $ secondaryCurrency privata; [...] funzione setPrimaryCurrency ($ currency) $ this-> primaryCurrency = $ currency;  function setSecondaryCurrency ($ currency) $ this-> secondaryCurrency = $ currency;  function getCurrencies () return array ('primary' => $ this-> primaryCurrency, 'secondary' => $ this-> secondaryCurrency); 

Per il momento, stiamo mantenendo la valuta come una semplice stringa. Questo potrebbe cambiare in futuro, ma non siamo ancora arrivati.


Dammi i soldi

Ci sono infinite ragioni per non rappresentare il denaro come un valore semplice. Calcoli in virgola mobile? Chiunque? Per quanto riguarda i frazionari di valuta? Dovremmo avere 10, 100 o 1000 centesimi in qualche valuta esotica? Bene, questo è un altro problema che dovremo evitare. Che dire dell'allocazione di centesimi indivisibili?

Ci sono troppi problemi esotici quando si lavora con i soldi per scriverli in codice, quindi andremo direttamente alla soluzione, il modello Money. Questo è un modello abbastanza semplice, con grandi vantaggi e molti casi d'uso, molto al di fuori del dominio finanziario. Ogni volta che devi rappresentare una coppia di unità di valore dovresti probabilmente usare questo schema.


The Money Pattern è fondamentalmente una classe che incapsula una quantità e una valuta. Quindi definisce tutte le operazioni matematiche sul valore rispetto alla valuta. "Assegnare ()" è una funzione speciale per distribuire una quantità specifica di denaro tra due o più destinatari.

Quindi, come utente di I soldi Mi piacerebbe poterlo fare in una prova:

class MoneyTest estende PHPUnit_Framework_TestCase function testWeCanCreateAMoneyObject () $ money = new Money (100, Currency :: USD ()); 

Ma non funzionerà ancora. Abbiamo bisogno di entrambi I soldi e Moneta. Ancor più, abbiamo bisogno Moneta prima I soldi. Questa sarà una classe semplice, quindi per ora salterò il test. Sono abbastanza sicuro che l'IDE possa generare la maggior parte del codice per me.

valuta di classe $ cent Factor privato; private $ stringRepresentation; funzione privata __construct ($ centFactor, $ stringRepresentation) $ this-> centFactor = $ centFactor; $ this-> stringRepresentation = $ stringRepresentation;  public function getCentFactor () return $ this-> centFactor;  function getStringRepresentation () return $ this-> stringRepresentation;  funzione statica USD () return new self (100, 'USD');  funzione statica EUR () return new self (100, 'EUR'); 

Questo è abbastanza per il nostro esempio. Abbiamo due funzioni statiche per le valute USD e EUR. In un'applicazione reale, avremmo probabilmente un costruttore generale con un parametro e caricare tutte le valute da una tabella di database o, ancora meglio, da un file di testo.

Successivamente, includi i due nuovi file nel test:

require_once '... /Currency.php'; require_once '... /Money.php'; class MoneyTest estende PHPUnit_Framework_TestCase function testWeCanCreateAMoneyObject () $ money = new Money (100, Currency :: USD ()); 

Questo test fallisce ancora, ma almeno può trovare Moneta adesso. Continuiamo con un minimo I soldi implementazione. Un po 'più di ciò che questo test richiede rigorosamente poiché è, ancora una volta, il codice per lo più generato automaticamente.

classe Money importo privato $; valuta $ privata; function __construct ($ amount, Currency $ currency) $ this-> amount = $ amount; $ this-> currency = $ currency; 

Si prega di notare che applichiamo il tipo Moneta per il secondo parametro nel nostro costruttore. Questo è un buon modo per evitare che i nostri clienti inviano messaggi indesiderati come valuta.


Confronto di denaro

La prima cosa che mi è venuta in mente dopo aver installato l'oggetto minimo è stata la necessità di confrontare in qualche modo gli oggetti in denaro. Poi ho ricordato che PHP è abbastanza intelligente quando si tratta di confrontare gli oggetti, quindi ho scritto questo test.

function testItCanTellTwoMoneyObjectAreEqual () $ m1 = new Money (100, Currency :: USD ()); $ m2 = new Money (100, Currency :: USD ()); $ This-> assertEquals ($ m1, m2 $); $ this-> assertTrue ($ m1 == $ m2); 

Bene, questo in realtà passa. Il "assertEquals" funzione può confrontare i due oggetti e anche la condizione di uguaglianza integrata da PHP "==" mi sta dicendo cosa mi aspetto Bello.

Ma che dire se siamo interessati a uno più grande dell'altro? Con mia sorpresa ancora maggiore, anche il seguente test passa senza problemi.

function testOneMoneyIsBiggerThanTheOther () $ m1 = new Money (200, Currency :: USD ()); $ m2 = new Money (100, Currency :: USD ()); $ this-> assertGreaterThan ($ m2, $ m1); $ this-> assertTrue ($ m1> $ m2); 

Il che ci porta a ...

function testOneMoneyIsLessThanTheOther () $ m1 = new Money (100, Currency :: USD ()); $ m2 = new Money (200, Currency :: USD ()); $ this-> assertLessThan ($ m2, $ m1); $ This-> assertTrue ($ m1 < $m2); 

... un test che passa immediatamente.


Più, meno, moltiplica

Vedendo così tanta magia PHP in realtà lavorando con i confronti, non ho potuto resistere a provare questo.

function testTwoMoneyObjectsCanBeAdded () $ m1 = new Money (100, Currency :: USD ()); $ m2 = new Money (200, Currency :: USD ()); $ sum = new Money (300, Currency :: USD ()); $ this-> assertEquals ($ sum, $ m1 + $ m2); 

Che fallisce e dice:

Oggetto della classe Money non può essere convertito in int

Hmm. Sembra abbastanza ovvio. A questo punto dobbiamo prendere una decisione. È possibile continuare questo esercizio con ancora più magia PHP, ma questo approccio, a un certo punto, trasformerà questo tutorial in un cheatheet di PHP anziché in uno schema di progettazione. Quindi, prendiamo la decisione di implementare i metodi effettivi per aggiungere, sottrarre e moltiplicare gli oggetti monetari.

function testTwoMoneyObjectsCanBeAdded () $ m1 = new Money (100, Currency :: USD ()); $ m2 = new Money (200, Currency :: USD ()); $ sum = new Money (300, Currency :: USD ()); $ this-> assertEquals ($ sum, $ m1-> add ($ m2)); 

Anche questo test fallisce, ma con un errore che ci dice che non c'è "Inserisci" metodo attivo I soldi.

funzione pubblica getAmount () return $ this-> amount;  function add ($ other) return new Money ($ this-> amount + $ other-> getAmount (), $ this-> currency); 

Per riassumere due I soldi oggetti, abbiamo bisogno di un modo per recuperare la quantità dell'oggetto che passiamo come argomento. Preferisco scrivere un getter, ma impostare la variabile di classe come pubblica sarebbe anche una soluzione accettabile. Ma cosa succede se vogliamo aggiungere Dollari agli Euro?

/ ** * @expectedException Exception * @expectedExceptionMessage Entrambi i soldi devono essere della stessa valuta * / function testItThrowsExceptionIfWeTryToAddTwoMoneysWithDifferentCurrency () $ m1 = new Money (100, Currency :: USD ()); $ m2 = new Money (100, Currency :: EUR ()); $ M1-> aggiungere ($ m2); 

Ci sono diversi modi per gestire le operazioni I soldi oggetti con valute diverse. Lanceremo un'eccezione e la aspettiamo nel test. In alternativa, potremmo implementare un meccanismo di conversione di valuta nella nostra applicazione, chiamarlo, convertirli entrambi I soldi oggetti in alcune valute predefinite e confrontarle. Oppure, se avessimo un algoritmo di conversione di valuta più sofisticato, potremmo sempre convertire da uno all'altro e confrontare in quella valuta convertita. Il fatto è che, quando la conversione avrà luogo, le tasse di conversione devono essere considerate e le cose si complicheranno. Quindi gettiamo quell'eccezione e andiamo avanti.

funzione pubblica getCurrency () return $ this-> currency;  function add (Money $ other) $ this-> ensureSameCurrencyWith ($ altro); restituire nuovo denaro ($ this-> amount + $ other-> getAmount (), $ this-> currency);  private function sureSameCurrencyWith (Money $ other) if ($ this-> currency! = $ other-> getCurrency ()) lancia una nuova Exception ("Both Moneys deve essere della stessa valuta"); 

Così va meglio. Facciamo un controllo per vedere se le valute sono diverse e lanciare un'eccezione. L'ho già scritto come metodo privato separato, perché so che ne avremo bisogno anche nelle altre operazioni matematiche.

La sottrazione e la moltiplicazione sono molto simili alle aggiunte, quindi ecco il codice e puoi trovare i test nel codice sorgente allegato.

function sottrarre (Money $ altro) $ this-> ensureSameCurrencyWith ($ altro); se ($ altro> $ questo) lancia una nuova Eccezione ("Il denaro sottratto è più di quello che abbiamo"); restituire nuovo denaro ($ this-> amount - $ other-> getAmount (), $ this-> currency);  function moltipliclyBy ($ moltiplicatore, $ roundMethod = PHP_ROUND_HALF_UP) $ product = round ($ this-> amount * $ moltiplicatore, 0, $ roundMethod); restituire nuovi soldi ($ prodotto, $ this-> valuta); 

Con la sottrazione, dobbiamo assicurarci di avere abbastanza soldi e con la moltiplicazione, dobbiamo agire per arrotondare le cose in alto o in basso in modo che la divisione (moltiplicazione con i numeri meno di una) non produca "mezzi centesimi". Manteniamo il nostro importo in centesimi, il più basso fattore possibile della valuta. Non possiamo dividerlo di più.


Presentazione della valuta sul nostro conto

Abbiamo un quasi completo I soldi e Moneta. È tempo di presentare questi oggetti account. Inizieremo con Moneta, e cambiare i nostri test di conseguenza.

function testItCanHavePrimaryAndSecondaryCurrencies () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ This-> Account-> setSecondaryCurrency (valuta :: USD ()); $ this-> assertEquals (array ('primary' => Currency :: EUR (), 'secondary' => Currency :: USD ()), $ this-> account-> getCurrencies ()); 

A causa della natura di digitazione dinamica di PHP, questo test passa senza problemi. Comunque mi piacerebbe forzare i metodi in account usare Moneta oggetti e non accettare altro. Questo non è obbligatorio, ma trovo questo tipo di suggerimento di tipo estremamente utile quando qualcun altro ha bisogno di capire il nostro codice.

function setPrimaryCurrency (Currency $ currency) $ this-> primaryCurrency = $ currency;  function setSecondaryCurrency (Currency $ currency) $ this-> secondaryCurrency = $ currency; 

Ora è ovvio a chiunque legga questo codice per la prima volta account lavora con Moneta.


Presentazione di denaro sul nostro conto

Le due azioni di base che ogni account deve fornire sono: deposito - ovvero l'aggiunta di denaro a un account - e il prelievo - ovvero la rimozione di denaro da un account. Il deposito ha una fonte e il ritiro ha una destinazione diversa dal nostro conto corrente. Non entreremo nei dettagli su come implementare queste transazioni, ci concentreremo solo sull'implementazione degli effetti che questi hanno sul nostro account. Quindi, possiamo immaginare un test come questo per il deposito.

function testAccountCanDepositMoney () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ money = new Money (100, Currency :: EUR ()); // Questo è 1 EURO $ questo-> account-> deposito ($ denaro); $ this-> assertEquals ($ money, $ this-> account-> getPrimaryBalance ()); 

Questo ci costringerà a scrivere un bel po 'di codice di implementazione.

account di classe private $ id; private $ primaryCurrency; $ secondaryCurrency privata; private $ secondaryBalance; private $ primaryBalance; function getSecondaryBalance () return $ this-> secondaryBalance;  function getPrimaryBalance () return $ this-> primaryBalance;  function __construct ($ id) $ this-> id = $ id;  [...] deposito di funzione (Money $ money) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ money: $ this-> secondaryBalance = $ money; 

OK OK. Lo so, ho scritto più di quello che era assolutamente necessario, per la produzione. Ma non voglio annoiarvi a morte con piccoli passi e sono anche abbastanza sicuro del codice secondaryBalance funzionerà correttamente È stato quasi interamente generato dall'IDE. Salterò persino a testarlo. Mentre questo codice fa passare il test, dobbiamo chiederci cosa succede quando effettuiamo depositi successivi? Vogliamo che i nostri soldi vengano aggiunti al saldo precedente.

function testSubsequentDepositsAddUpTheMoney () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ money = new Money (100, Currency :: EUR ()); // Questo è 1 EURO $ questo-> account-> deposito ($ denaro); // Un euro nell'account $ this-> account-> deposit ($ money); // Due euro nel conto $ this-> assertEquals ($ money-> multiplyBy (2), $ this-> account-> getPrimaryBalance ()); 

Bene, questo fallisce. Quindi dobbiamo aggiornare il nostro codice di produzione.

deposito di funzione (Money $ money) if ($ this-> primaryCurrency == $ money-> getCurrency ()) $ this-> primaryBalance = $ this-> primaryBalance? : new Money (0, $ this-> primaryCurrency); $ this-> primaryBalance = $ this-> primaryBalance-> add ($ money);  else $ this-> secondaryBalance = $ this-> secondaryBalance? : new Money (0, $ this-> secondaryCurrency); $ this-> secondaryBalance = $ this-> secondaryBalance-> add ($ money); 

Questo è molto meglio. Probabilmente abbiamo finito con depositare metodo e possiamo continuare con ritirarsi.

function testAccountCanWithdrawMoneyOfSameCurrency () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ money = new Money (100, Currency :: EUR ()); // Questo è 1 EURO $ questo-> account-> deposito ($ denaro); $ this-> account-> withdraw (new Money (70, Currency :: EUR ())); $ this-> assertEquals (new Money (30, Currency :: EUR ()), $ this-> account-> getPrimaryBalance ()); 

Questo è solo un semplice test. La soluzione è semplice, anche.

function withdraw (Money $ money) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> sottrarre ($ money): $ this-> secondaryBalance = $ this-> secondaryBalance-> sottrarre ($ money); 

Bene, questo funziona, ma cosa succede se vogliamo usare a Moneta non è nel nostro account? Dovremmo lanciare un'Escizione per questo.

/ ** * @expectedException Exception * @expectedExceptionMessage Questo account non ha valuta USD * / function testThrowsExceptionForInexistentCurrencyOnWithdraw () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ money = new Money (100, Currency :: EUR ()); // Questo è 1 EURO $ questo-> account-> deposito ($ denaro); $ this-> account-> withdraw (new Money (70, Currency :: USD ())); 

Questo ci costringerà anche a controllare le nostre valute.

funzione di prelievo (Money $ money) $ this-> validateCurrencyFor ($ money); $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> sottrarre ($ money): $ this-> secondaryBalance = $ this-> secondaryBalance-> sottrarre ($ money);  funzione privata validateCurrencyFor (Money $ money) if (! in_array ($ money-> getCurrency (), $ this-> getCurrencies ())) genera una nuova Exception (sprintf ('Questo account non ha valuta% s', $ money -> getCurrency () -> getStringRepresentation ())); 

Ma cosa succede se vogliamo ritirare più di quello che abbiamo? Quel caso era già stato affrontato quando abbiamo implementato la sottrazione su I soldi. Ecco il test che lo dimostra.

/ ** * @expectedException Exception * @expectedExceptionMessage Il denaro sottratto è più di quello che abbiamo * / function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ money = new Money (100, Currency :: EUR ()); // Questo è 1 EURO $ questo-> account-> deposito ($ denaro); $ this-> account-> withdraw (new Money (150, Currency :: EUR ())); 

Trattare con Ritirare e Scambiare

Uno degli aspetti più difficili da affrontare quando lavoriamo con più valute è lo scambio tra di loro. La bellezza di questo modello di progettazione è che ci consente di semplificare un po 'questo problema isolandolo e incapsulandolo nella sua stessa classe. Mentre la logica in un Scambio la classe può essere molto sofisticata, il suo uso diventa molto più facile. Per il bene di questo tutorial, immaginiamo di averne alcune di base Scambio solo logica 1 EUR = 1,5 USD.

class Exchange function convert (Money $ money, Currency $ toCurrency) if ($ toCurrency == Currency :: EUR () && $ money-> getCurrency () == Currency :: USD ()) restituisce new Money ($ money -> multiplyBy (0.67) -> getAmount (), $ toCurrency); if ($ toCurrency == Currency :: USD () && $ money-> getCurrency () == Currency :: EUR ()) restituisce new Money ($ money-> multiplyBy (1.5) -> getAmount (), $ toCurrency) ; restituire $ soldi; 

Se convertiamo da EUR a USD moltiplichiamo il valore di 1,5, se convertiamo da USD a EUR dividiamo il valore per 1,5, altrimenti presumiamo di convertire due valute dello stesso tipo, quindi non facciamo nulla e restituiamo solo i soldi . Certo, in realtà questa sarebbe una classe molto più complicata.

Ora, avendo un Scambio classe, account può prendere decisioni diverse quando vogliamo ritirarci I soldi in una valuta, ma non abbiamo abbastanza in questa valuta specifica. Ecco un test che meglio lo esemplifica.

function testItConvertsMoneyFromTheOtherCurrencyWhenWeDoNotHaveEnoughInTheCurrentOne () $ this-> account-> setPrimaryCurrency (Valuta :: USD ()); $ money = new Money (100, Currency :: USD ()); // Questo è 1 USD $ this-> account-> deposito ($ denaro); $ This-> Account-> setSecondaryCurrency (Valuta :: EUR ()); $ money = new Money (100, Currency :: EUR ()); // Questo è 1 EURO = 1,5 USD $ questo-> account-> deposito ($ denaro); $ this-> account-> withdraw (new Money (200, Currency :: USD ())); // Questo è 2 USD $ this-> assertEquals (new Money (0, Currency :: USD ()), $ this-> account-> getPrimaryBalance ()); $ this-> assertEquals (new Money (34, Currency :: EUR ()), $ this-> account-> getSecondaryBalance ()); 

Impostiamo la valuta principale del nostro conto in USD e depositiamo un dollaro. Quindi impostiamo la valuta secondaria in EUR e depositiamo un euro. Quindi ritiriamo due dollari. Infine, prevediamo di rimanere a zero dollari e 0,34 euro. Ovviamente questo test genera un'eccezione, quindi dobbiamo implementare una soluzione a questo dilemma.

funzione di prelievo (Money $ money) $ this-> validateCurrencyFor ($ money); if ($ this-> primaryCurrency == $ money-> getCurrency ()) if ($ this-> primaryBalance> = $ money) $ this-> primaryBalance = $ this-> primaryBalance-> sottrarre ($ money);  else $ ourMoney = $ this-> primaryBalance-> add ($ this-> secondaryToPrimary ()); $ restanteMoney = $ ourMoney-> sottrarre ($ money); $ this-> primaryBalance = new Money (0, $ this-> primaryCurrency); $ this-> secondaryBalance = (new Exchange ()) -> convert ($ remainingMoney, $ this-> secondaryCurrency);  else $ this-> secondaryBalance = $ this-> secondaryBalance-> sottrarre ($ money);  private function secondaryToPrimary () return (new Exchange ()) -> convert ($ this-> secondaryBalance, $ this-> primaryCurrency); 

Wow, molte modifiche sono state apportate per supportare questa conversione automatica. Quello che sta succedendo è che se siamo nel caso dell'estrazione dalla nostra valuta principale e non abbiamo abbastanza soldi, convertiamo il nostro saldo della valuta secondaria in primaria e proviamo di nuovo la sottrazione. Se non abbiamo ancora abbastanza soldi, il $ ourMoney l'oggetto genererà l'eccezione appropriata. Altrimenti, imposteremo il nostro saldo primario a zero e convertiremo il denaro residuo nella valuta secondaria e imposteremo il nostro saldo secondario su tale valore.

Resta in regola con la logica del nostro account implementare una conversione automatica simile per la valuta secondaria. Non implementeremo una logica così simmetrica. Se ti piace l'idea, considerala come un esercizio per te. Inoltre, pensa a un metodo privato più generico che farebbe la magia delle conversioni automatiche in entrambi i casi.

Questo complesso cambiamento alla nostra logica ci costringe anche ad aggiornare un altro dei nostri test. Ogni volta che vogliamo convertire automaticamente dobbiamo avere un saldo, anche se è solo zero.

/ ** * @expectedException Exception * @expectedExceptionMessage Il denaro sottratto è più di quello che abbiamo * / function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ money = new Money (100, Currency :: EUR ()); // Questo è 1 EURO $ questo-> account-> deposito ($ denaro); $ This-> Account-> setSecondaryCurrency (valuta :: USD ()); $ money = new Money (0, Currency :: USD ()); $ This-> Account-> deposito ($ denaro); $ this-> account-> withdraw (new Money (150, Currency :: EUR ())); 

Allocazione di denaro tra conti

L'ultimo metodo su cui dobbiamo implementare I soldi è assegnare. Questa è la logica che decide cosa fare dividendo i soldi tra conti diversi che non possono essere fatti esattamente. Ad esempio, se abbiamo 0,10 centesimi e vogliamo allocarli tra due account in una proporzione di 30-70 percentuali, è facile. Un account riceverà tre centesimi e l'altro sette. Tuttavia, se vogliamo fare la stessa ripartizione del rapporto 30-70 di cinque centesimi, abbiamo un problema. L'allocazione esatta sarebbe di 1,5 cent in un conto e 3,5 nell'altro. Ma non possiamo dividere i centesimi, quindi dobbiamo implementare il nostro algoritmo per allocare i soldi.

Possono esserci diverse soluzioni a questo problema, un algoritmo comune consiste nell'aggiungere un centesimo in sequenza a ciascun account. Se un account ha più centesimi del suo valore matematico esatto, dovrebbe essere eliminato dalla lista di assegnazione e non ricevere ulteriori fondi. Ecco una rappresentazione grafica.


E un test per dimostrare che il nostro punto è sotto.

function testItCanAllocateMoneyBetween2Accounts () $ a1 = $ this-> anAccount (); $ a2 = $ this-> anAccount (); $ money = new Money (5, Currency :: USD ()); $ money-> allocate ($ a1, $ a2, 30, 70); $ this-> assertEquals (new Money (2, Currency :: USD ()), $ a1-> getPrimaryBalance ()); $ this-> assertEquals (new Money (3, Currency :: USD ()), $ a2-> getPrimaryBalance ());  funzione privata anAccount () $ account = new Account (1); $ Account-> setPrimaryCurrency (valuta :: USD ()); $ account-> deposit (new Money (0, Currency :: USD ())); return $ account; 

Creiamo solo un I soldi oggetto con cinque centesimi e due account. Noi chiamiamo assegnare e si aspettano che i due o tre valori siano nei due account. Abbiamo anche creato un metodo di supporto per creare rapidamente account. Il test fallisce, come previsto, ma possiamo farlo passare abbastanza facilmente.

funzione allocate (Account $ a1, Account $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> amount * $ a1Percent / 100; $ exactA2Balance = $ this-> amount * $ a2Percent / 100; $ oneCent = new Money (1, $ this-> currency); while ($ this-> amount> 0) if ($ a1-> getPrimaryBalance () -> getAmount () < $exactA1Balance)  $a1->deposito ($ oneCent); $ This-> amount--;  if ($ this-> amount <= 0) break; if ($a2->getPrimaryBalance () -> getAmount () < $exactA2Balance)  $a2->deposito ($ oneCent); $ This-> amount--; 

Bene, non il codice più semplice, ma funziona correttamente, come dimostra il superamento del nostro test. L'unica cosa che possiamo ancora fare a questo codice è ridurre la piccola duplicazione all'interno di mentre ciclo continuo.

funzione allocate (account $ a1, account $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> amount * $ a1Percent / 100; $ exactA2Balance = $ this-> amount * $ a2Percent / 100; while ($ this-> amount> 0) $ this-> allocateTo ($ a1, $ exactA1Balance); se ($ questo-> importo <= 0) break; $this->allocateTo ($ a2, $ exactA2Balance);  private function allocateTo ($ account, $ exactBalance) if ($ account-> getPrimaryBalance () -> getAmount () < $exactBalance)  $account->deposito (nuovo denaro (1, $ this-> valuta)); $ This-> amount--; 

Pensieri finali

Quello che trovo sorprendente con questo piccolo schema è l'ampia gamma di casi in cui possiamo applicarlo.

Abbiamo finito con il nostro modello di denaro. Abbiamo visto che è un modello abbastanza semplice, che incapsula le specifiche del concetto di denaro. Abbiamo anche visto che questo incapsulamento allevia il peso dei calcoli dall'account. L'account può concentrarsi sulla rappresentazione del concetto da un livello superiore, dal punto di vista della banca. L'account può implementare metodi come la connessione con titolari di account, ID, transazioni e denaro. Sarà un orchestratore non un calcolatore. Il denaro si prenderà cura dei calcoli.

Quello che trovo sorprendente con questo piccolo schema è l'ampia gamma di casi in cui possiamo applicarlo. Fondamentalmente, ogni volta che hai una coppia di unità di valore, puoi usarla. Immagina di avere un'applicazione meteo e vuoi implementare una rappresentazione per la temperatura. Questo sarebbe l'equivalente del nostro oggetto Money. Puoi usare Fahrenheit o Celsius come valute.

Un altro caso d'uso è quando hai un'applicazione di mappatura e vuoi rappresentare le distanze tra i punti. È possibile utilizzare facilmente questo modello per passare da misurazioni metriche o imperiali. Quando lavori con unità semplici, puoi rilasciare l'oggetto Exchange e implementare la semplice logica di conversione all'interno dell'oggetto "Money".

Spero che questo tutorial ti sia piaciuto e sono ansioso di conoscere i diversi modi in cui potresti utilizzare questo concetto. Grazie per aver letto.