Modelul banilor Calea corectă de reprezentare a perechilor de valori-unități

Modelul de bani, definit de Martin Fowler și publicat în modelele de aplicații de arhitectură pentru întreprinderi, este o modalitate excelentă de a reprezenta perechi de unități de valoare. Se numește Money Pattern deoarece a apărut într-un context financiar și vom ilustra utilizarea sa, în special în acest context, folosind PHP.


Un cont PayPal similar

Nu am nici o idee despre modul în care este implementat PayPal, dar cred că este o idee bună să-i luați funcționalitatea ca exemplu. Permiteți-mi să vă arăt ce vreau să spun, contul meu PayPal are două valute: dolari americani și euro. Păstrează cele două valori separate, dar pot primi bani în orice monedă, văd suma mea totală în oricare dintre cele două valute și pot extrage în oricare dintre cele două. Din motive de acest exemplu, imaginați-vă că extragem în oricare dintre monedele și conversia automată se face în cazul în care soldul acelei monede este mai mic decât ceea ce vrem să transferăm, dar totuși există încă destui bani în cealaltă monedă. De asemenea, vom limita exemplul la doar două valute.


Obținerea unui cont

Dacă aș fi creat și utilizat un obiect de cont, aș dori să îl inițializez cu un număr de cont.

function testItCanCrateANewAccount () $ this-> assertInstanceOf ("Cont", cont nou (123)); 

Acest lucru va fi în mod evident eșuat deoarece nu avem încă o clasă de cont.

cont de clasă 

Păi, scriind asta într-un nou "Account.php" fișier și cerându-l în test, a făcut-o să treacă. Totuși, acest lucru se face doar pentru a ne face confortabil cu ideea. În continuare, mă gândesc să obțin contul id.

funcția testItCanCrateANewAccountWithId () $ this-> assertEquals (123, (noul cont (123)) -> getId ()); 

De fapt, am schimbat testul anterior în acesta. Nu există niciun motiv să păstreze primul. A trăit viața, ceea ce înseamnă că m-au forțat să mă gândesc la asta Cont clasă și să o creeze. Acum putem trece mai departe.

clasa cont private $ id; funcția __construct ($ id) $ this-> id = $ id;  funcția publică getId () return $ this-> id; 

Testul trece și Cont începe să arate ca o clasă reală.


valute

Pe baza analogiei noastre PayPal, este posibil să dorim să definim o monedă primară și secundară pentru contul nostru.

cont privat $; funcția protejată setUp () $ this-> account = cont nou (123);  [...] funcția testItCanHavePrimaryAndSecondaryCurrencies () $ this-> account-> setPrimaryCurrency ("EUR"); $ This-> Cont-> setSecondaryCurrency ( 'USD'); $ this-> assertEquals (array ('primar' => 'EUR', 'secundar' => 'USD'), $ this-> account-> getCurrencies ()); 

Acum, testul de mai sus ne va obliga să scriem următorul cod.

clasa cont private $ id; privat $ primaryCurrency; private $ secondaryCurrency; [...] setul de funcțiiPrimea primarului ($ valută) $ this-> primaryCurrency = $ valută;  set setSursă secundară ($ valută) $ this-> secondaryCurrency = $ currency;  funcția getCurrencies () return array ('primary' => $ this-> primaryCurrency, 'secundar' => $ this-> secondaryCurrency); 

Pentru moment, păstrăm moneda ca un șir simplu. Acest lucru se poate schimba în viitor, dar nu suntem încă acolo.


Gimme the Money

Există motive nesfârșite de a nu reprezenta banii drept o valoare simplă. Calcularea punctelor în virgulă? Oricine? Ce zici de fracționarea monedei? Ar trebui să avem 10, 100 sau 1000 de cenți în unele monede exotice? Ei bine, aceasta este o altă problemă pe care o vom evita. Cum rămâne cu alocarea centilor indivizibili?

Există probleme prea multe și exotice atunci când lucrăm cu bani pentru a le scrie în cod, așa că vom merge direct la soluția Money Pattern. Acesta este un model simplu, cu avantaje mari și multe cazuri de utilizare, departe de domeniul financiar. Ori de câte ori trebuie să reprezinți o pereche de unități de valoare, probabil că ar trebui să utilizați acest model.


Modelul de bani este, în principiu, o clasă care încapsulează o sumă și o monedă. Apoi definește toate operațiile matematice cu privire la valoare în raport cu moneda. "aloca()" este o funcție specială de a distribui o anumită sumă de bani între doi sau mai mulți destinatari.

Deci, ca un utilizator de Bani Aș vrea să pot face acest lucru într-un test:

clasa MoneyTest extinde PHPUnit_Framework_TestCase function testWeCanCreateAMoneyObject () $ money = bani noi (100, valuta :: USD ()); 

Dar asta nu va funcționa încă. Avem nevoie de amândouă Bani și Valută. Chiar mai mult, avem nevoie Valută inainte de Bani. Aceasta va fi o clasă simplă, așa că voi trece peste testarea pentru moment. Sunt destul de sigur că IDE poate genera cea mai mare parte a codului pentru mine.

clasa Valuta private $ centFactor; private $ stringRepresentation; funcția privată __construct ($ centFactor, $ stringRepresentation) $ this-> centFactor = $ centFactor; $ this-> stringRepresent = $ stringReprezentare;  funcția publică getCentFactor () return $ this-> centFactor;  funcția getStringRepresentation () return $ this-> stringRepresentation;  funcția statică USD () retur noi noi (100, "USD");  funcția statică EUR () retur noi noi (100, "EUR"); 

Asta e suficient pentru exemplul nostru. Avem două funcții statice pentru monedele USD și EUR. Într-o aplicație reală, probabil că vom avea un constructor general cu un parametru și vom încărca toate monedele dintr-o tabelă de baze de date sau, chiar mai bine, dintr-un fișier text.

Apoi, includeți cele două fișiere noi în test:

requ_once '... /Currency.php'; requ_once '... /Money.php'; clasa MoneyTest extinde PHPUnit_Framework_TestCase function testWeCanCreateAMoneyObject () $ money = bani noi (100, valuta :: USD ()); 

Acest test încă nu reușește, dar cel puțin îl poate găsi Valută acum. Continuăm cu un minim Bani punerea în aplicare. Un pic mai mult decât ceea ce cere acest test strict, deoarece este, din nou, în mare parte codul auto-generat.

bani de clasă suma privată $; dolar privat $; funcția __construct (suma $, valuta $ valută) $ this-> amount = $ amount; $ this-> currency = $ valută; 

Rețineți că noi impunem tipul Valută pentru al doilea parametru în constructorul nostru. Aceasta este o modalitate frumoasă de a evita expedierea de către clienții noștri a banilor ca monedă.


Comparând Bani

Primul lucru care mi-a venit în minte după ce am pus în mișcare obiectul minimal a fost acela că va trebui să comparăm obiectele banilor într-un fel. Apoi mi-am amintit că PHP este destul de inteligent când vine vorba de compararea obiectelor, așa că am scris acest test.

function testItCanTellTwoMoneyObjectAreEqual () $ m1 = bani noi (100, valuta :: USD ()); $ m2 = bani noi (100, valuta :: USD ()); $ This-> assertEquals ($ m1, m2 $); $ this-> assertTrue ($ m1 == $ m2); 

Păi, asta trece. "assertEquals" funcția poate compara cele două obiecte și chiar condiția de egalitate încorporată din PHP "==" îmi spune ce mă aștept. Frumos.

Dar, dacă suntem interesați să fie una mai mare decât cealaltă? Spre surpriza mea și mai mare, următorul test trece și fără probleme.

funcția testOneMoneyIsBiggerThanTheOther () $ m1 = bani noi (200, Moneda :: USD ()); $ m2 = bani noi (100, valuta :: USD ()); $ acest-> assertGreaterThan ($ m2, $ m1); $ this-> assertTrue ($ m1> $ m2); 

Ceea ce ne face să ...

funcția testOneMoneyIsLessThanTheOther () $ m1 = bani noi (100, Moneda :: USD ()); $ m2 = bani noi (200, Moneda :: USD ()); $ this-> assertLessThan ($ m2, $ m1); $ This-> assertTrue (m1 $ < $m2); 

... un test care trece imediat.


Plus, Minus, Multiplicați

Văzând atât de mult magia PHP care lucrează cu comparații, nu am putut rezista să încerc asta.

function testTwoMoneyObjectsCanBeAdded () $ m1 = bani noi (100, valuta :: USD ()); $ m2 = bani noi (200, Moneda :: USD ()); $ sum = bani noi (300, valuta :: USD ()); $ this-> assertEquals (suma $, $ m1 + $ m2); 

Care nu reușește și spune:

Obiectul de clasă Bani nu a putut fi convertit la int

Hmm. Sună destul de evident. În acest moment trebuie să luăm o decizie. Este posibil să continuăm acest exercițiu chiar și cu mai multă magie PHP, dar această abordare va transforma, într-un anumit moment, acest tutorial într-un cheat de calcul PHP în loc de un model de design. Deci, să luăm decizia de a implementa metodele reale de adăugare, scădere și multiplicare a obiectelor de bani.

function testTwoMoneyObjectsCanBeAdded () $ m1 = bani noi (100, valuta :: USD ()); $ m2 = bani noi (200, Moneda :: USD ()); $ sum = bani noi (300, valuta :: USD ()); $ this-> assertEquals (suma $, $ m1-> add ($ m2)); 

Acest test nu reușește, dar cu o eroare care ne spune că nu există "adăuga" metoda pe Bani.

funcția publică getAmount () return $ this-> amount;  adăugați funcția ($ other) returnează bani noi ($ this-> amount + $ other-> getAmount (), $ this-> currency); 

Pentru a rezuma două Bani obiecte, avem nevoie de o modalitate de a recupera cantitatea obiectului pe care îl parcurgem ca argument. Prefer să scriu un getter, dar stabilirea variabilei de clasă ca fiind publică ar fi, de asemenea, o soluție acceptabilă. Dar dacă vrem să adăugăm Dolari la Euro?

/ ** * Excepție excepțională de așteptare * @exisiteExcepțieAnunțuri Ambele monede trebuie să fie de aceeași monedă * / funcție testItThrowsExceptionIfWeTryToAddTwoMoneysWithDifferentCurrency () $ m1 = bani noi (100, Moneda :: USD ()); $ m2 = bani noi (100, Moneda :: EUR ()); $ L1> adaugă ($ m2); 

Există mai multe modalități de a face față operațiunilor Bani obiecte cu valute diferite. Vom arunca o excepție și ne așteptăm la test. Alternativ, am putea pune în aplicare un mecanism de conversie a monedei în aplicația noastră, să-l numim, să convertim ambele Bani obiecte în altă valută implicită și le comparați. Sau, dacă am avea un algoritm de conversie a monedei mai sofisticat, am putea oricând să convertim de la unul la altul și să comparăm în acea monedă convertită. Chestia este că, odată cu transformarea, taxele de conversie trebuie să fie luate în considerare și lucrurile vor deveni destul de complicate. Deci, să aruncăm excepția și să mergem mai departe.

funcția publică getCurrency () return $ this-> currency;  funcția adăugați ($ $ alții) $ this-> ensureSameCurrencyWith ($ other); returnează bani noi ($ this-> amount + $ other-> getAmount (), $ this-> currency);  funcția privată ensuresSameCurrencyWith (Money $ other) if ($ this-> currency! = $ other-> getCurrency ()) aruncă o nouă excepție ("Ambele monede trebuie să fie de aceeași monedă"); 

Asa e mai bine. Facem un control pentru a vedea dacă monedele sunt diferite și aruncă o excepție. Am scris-o deja ca o metodă privată separată, pentru că știu că o vom avea nevoie și de celelalte operații matematice.

Scăderea și multiplicarea sunt foarte asemănătoare adunării, deci aici este codul și puteți găsi testele în codul sursă atașat.

funcția scade (alte $ bani) $ this-> ensureSameCurrencyWith ($ other); dacă ($ alt> $ this) aruncă o nouă excepție ("banii scutiți sunt mai mult decât ceea ce avem"); returnați bani noi ($ this-> amount - $ other-> getAmount (), $ this-> currency);  funcția multiplyBy ($ multiplicator, $ roundMethod = PHP_ROUND_HALF_UP) $ product = runda ($ this-> suma * $ multiplicator, 0, $ roundMethod); returnați bani noi ($ product, $ this-> currency); 

Cu scăderea, trebuie să ne asigurăm că avem destui bani și cu multiplicare, trebuie să luăm măsuri pentru a rotunzi lucrurile în sus sau în jos, astfel încât împărțirea (multiplicarea cu numere mai mici decât una) să nu producă "jumătate de cenți". Ne păstrăm suma în centi, cel mai mic factor posibil al monedei. Nu o putem împărți mai mult.


Introducerea valutei în Contul nostru

Avem un proiect aproape complet Bani și Valută. Este timpul să introduceți aceste obiecte Cont. Vom începe cu asta Valută, și să schimbe testele noastre în consecință.

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

Din cauza naturii dinamice de tiparire a PHP, acest test trece fără probleme. Cu toate acestea, aș dori să forțeze metodele din Cont a folosi Valută obiecte și nu acceptă nimic altceva. Acest lucru nu este obligatoriu, dar găsesc aceste tipuri de aluzii de tip extrem de utile atunci când altcineva trebuie să înțeleagă codul nostru.

set de funcțiiPrimea primar (valută $ valută) $ this-> primaryCurrency = $ valută;  set setSecondaryMerna (valută $ valută) $ this-> secondaryCurrency = $ valută; 

Acum este evident pentru oricine citește acest cod pentru prima dată Cont Functioneaza cu Valută.


Introducerea de bani în contul nostru

Cele două acțiuni de bază pe care orice cont trebuie să le furnizeze este: depozit - adică adăugarea de bani într-un cont - și retragerea - adică eliminarea banilor dintr-un cont. Depunerea are o sursă, iar retragerea are o altă destinație decât contul nostru curent. Nu vom trece în detaliu despre modul de implementare a acestor tranzacții, vom concentra doar asupra implementării efectelor pe care le au asupra noastră. Deci, ne putem imagina un test ca acesta pentru depunere.

funcția testAccountCanDepositMoney () $ this-> account-> setPrimaryMoney (Moneda :: EUR ()); $ bani = bani noi (100, Moneda :: EUR ()); // Aceasta este 1 EURO $ acest cont-> depozit ($ bani); $ this-> assertEquals ($ bani, $ acest-> cont-> getPrimaryBalance ()); 

Acest lucru ne va forța să scriem o mulțime de cod de implementare.

clasa cont private $ id; privat $ primaryCurrency; private $ secondaryCurrency; privat $ secondaryBalance; privat $ primaryBalance; funcția getSecondaryBalance () return $ this-> secondaryBalance;  funcția getPrimaryBalance () return $ this-> primaryBalance;  funcția __construct ($ id) $ this-> id = $ id;  [...] depozit funcțional (bani bani $) $ this-> primaryCurrency == $ bani-> getCurrency ()? $ this-> primaryBalance = $ bani: $ this-> secondaryBalance = $ bani; 

BINE BINE. Știu, am scris mai mult decât era absolut necesar pentru producție. Dar nu vreau să te duc la moarte cu pași pentru copii și sunt, de asemenea, destul de sigur de cod secondaryBalance va funcționa corect. Era aproape generat în întregime de IDE. Voi încerca chiar să o testez. În timp ce acest cod face testul nostru, trebuie să ne întrebăm ce se întâmplă atunci când facem depozite ulterioare? Vrem ca banii noștri să fie adăugați la soldul anterior.

funcția testSubsequentDepositsAddUpTheMoney () $ this-> account-> setPrimaryCurrency (Moneda :: EUR ()); $ bani = bani noi (100, Moneda :: EUR ()); // Aceasta este 1 EURO $ acest cont-> depozit ($ bani); // Un euro în contul $ this-> account-> deposit ($ money); // Două euro în contul $ this-> assertEquals ($ bani-> multiplyBy (2), $ this-> account-> getPrimaryBalance ()); 

Ei bine, asta nu reușește. Deci trebuie să ne actualizăm codul de producție.

($ this-> primaryCurrency == $ bani-> getCurrency ()) $ this-> primaryBalance = $ this-> primalBalance? : bani noi (0, $ this-> primaryCurrency); $ this-> primaryBalance = $ this-> primaryBalance-> adăugați ($ bani);  altceva $ this-> secondaryBalance = $ this-> secondaryBalance? : bani noi (0, $ this-> secondaryCurrency); $ this-> secondaryBalance = $ this-> secondaryBalance-> adăugați ($ bani); 

Acest lucru este mult mai bun. Probabil că am terminat cu depozit și putem continua retrage.

funcția testAccountCanWithdrawMoneyOfSameCurrency () $ this-> account-> setPrimaryCurrency (Moneda :: EUR ()); $ bani = bani noi (100, Moneda :: EUR ()); // Aceasta este 1 EURO $ acest cont-> depozit ($ bani); $ this-> account-> withdraw (bani noi (70, Moneda :: EUR ())); $ this-> assertEquals (noul Bani (30, Moneda :: EUR ()), $ this-> account-> getPrimaryBalance ()); 

Acesta este doar un simplu test. De asemenea, soluția este simplă.

($ bani bani) $ this-> primaryCurrency == $ bani-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> scădea ($ bani): $ this-> secondaryBalance = $ this-> secondaryBalance-> scade ($ bani); 

Pai, asta functioneaza, dar daca am vrea sa folosim a Valută care nu este în contul nostru? Ar trebui să aruncăm o Excpeție pentru asta.

/ ** * Excepție excepțională de excepție * @expectedExceptionMessage Acest cont nu are nicio monedă USD * / funcția testThrowsExceptionForInexistentCurrencyOnWithdraw () $ this-> account-> setPrimaryCurrency (Moneda :: EUR ()); $ bani = bani noi (100, Moneda :: EUR ()); // Aceasta este 1 EURO $ acest cont-> depozit ($ bani); $ this-> account-> withdraw (bani noi (70, valuta :: USD ())); 

Și asta ne va forța să ne verificăm monedele.

funcția retrage (bani bani bani) $ this-> validateCurrencyFor ($ bani); $ this-> primaryCurrency == $ bani-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> scădea ($ bani): $ this-> secondaryBalance = $ this-> secondaryBalance-> scade ($ bani);  funcția privată validateCurrencyFor (Money $ money) if (! in_array ($ money-> getCurrency (), $ this-> getCurrencies -> getCurrency () -> getStringRepresentation ())); 

Dar dacă vrem să ne retragem mai mult decât ceea ce avem? Acest caz a fost deja abordat când am implementat scăderea Bani. Iată testul care o dovedește.

/ ** * Excepție de excepție excepțională * @expectedExceptionMessage Banii scutiți sunt mai mult decât ceea ce avem * / function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Moneda :: EUR ()); $ bani = bani noi (100, Moneda :: EUR ()); // Aceasta este 1 EURO $ acest cont-> depozit ($ bani); $ this-> account-> withdraw (bani noi (150, Moneda :: EUR ())); 

Confruntarea cu retragerea și schimbul

Unul dintre lucrurile mai dificile cu care se confruntă când lucrăm cu mai multe valute este schimbul între ele. Frumusețea acestui model de design este că ne permite să simplificăm oarecum această problemă prin izolarea și încapsularea acesteia în propria clasă. În timp ce logica într-un schimb valutar clasa poate fi foarte sofisticată, utilizarea sa devine mult mai ușoară. De dragul acestui tutorial, să ne imaginăm că avem unele foarte fundamentale schimb valutar numai logica. 1 EUR = 1,5 USD.

clasa Exchange function convert (bani $ bani, valuta $ toCurrency) if ($ toCurrency == valuta :: EUR () && $ money-> getCurrency () == valuta :: USD -> multiplicBy (0,67) -> getAmount (), $ laCurrency); dacă ($ toCurrency == Moneda :: USD () && $ money-> getCurrency () == Moneda :: EUR ()) returnează bani noi ($ money-> multiplyBy (1.5) -> getAmount (), $ toCurrency) ; returnați $ bani; 

Dacă convertim de la EUR la USD, vom înmulți valoarea cu 1.5, dacă convertim de la USD la EUR, vom împărți valoarea cu 1.5, în caz contrar presupunem că convertim două valute de același tip, deci nu facem nimic și doar returnăm banii . Desigur, în realitate, aceasta ar fi o clasă mult mai complicată.

Acum, având un schimb valutar clasă, Cont pot lua decizii diferite atunci când vrem să ne retragem Bani într-o monedă, dar nu ajungem suficient în această monedă. Iată un test care exemplifică mai bine acest lucru.

funcția testItConvertsMoneyFromTheOtherCurrencyWhenWeDoNotHaveEnoughInTheCurrentOne () $ this-> account-> setPrimaryCurrency (Moneda :: USD ()); $ bani = bani noi (100, valuta :: USD ()); // Aceasta este 1 USD $ this-> account-> deposit ($ bani); $ This-> Cont-> setSecondaryCurrency (valuta :: EUR ()); $ bani = bani noi (100, Moneda :: EUR ()); // Aceasta este 1 EURO = 1,5 USD $ acest cont-> depozit ($ bani); $ this-> account-> withdraw (bani noi (200, Moneda :: USD ())); // Aceasta este 2 USD $ this-> assertEquals (bani noi (0, valuta :: USD ()), $ this-> account-> getPrimaryBalance ()); $ this-> assertEquals (bani noi (34, valuta :: EUR ()), $ this-> account-> getSecondaryBalance ()); 

Am stabilit moneda primară a contului nostru în USD și am depus un dolar. Apoi am stabilit moneda secundară la EUR și am depus un euro. Apoi, retragem doi dolari. În cele din urmă, ne așteptăm să rămânem la zero și la 0,34 euro. Desigur, acest test aruncă o excepție, așa că trebuie să implementăm o soluție la această dilemă.

funcția retrage (bani bani bani) $ this-> validateCurrencyFor ($ bani); dacă ($ this-> primaryCurrency == $ money-> getCurrency ()) dacă ($ this-> primaryBalance> = $ bani) $ this-> primBalance = $ this-> primBalance-> scade ($ bani);  altceva $ ourMoney = $ this-> primaryBalance-> add ($ this-> secondaryToPrimary ()); $ remaMoney = $ ourMoney-> scădea ($ bani); $ this-> primaryBalance = bani noi (0, $ this-> primaryCurrency); $ this-> secondaryBalance = (noul Exchange ()) -> convertire ($ remainingMoney, $ this-> secondaryCurrency);  altceva $ this-> secondaryBalance = $ this-> secondaryBalance-> scădea ($ bani);  funcția privată secondaryToPrimary () retur (nou Exchange ()) -> convertire ($ this-> secondaryBalance, $ this-> primaryCurrency); 

Unele schimbări trebuiau făcute pentru a susține această conversie automată. Ceea ce se întâmplă este că, dacă ne aflăm în cazul extragerii din moneda noastră primară și nu avem destui bani, transformăm balanța noastră de monedă secundară în primar și încercăm din nou scăderea. Dacă încă nu avem destui bani, $ ourMoney obiect va arunca excepția corespunzătoare. În caz contrar, vom stabili soldul primar la zero și vom transforma banii rămași înapoi în moneda secundară și vom stabili soldul secundar la valoarea respectivă.

Rămâne până la logica contului nostru de a implementa o conversie automată similară pentru moneda secundară. Nu vom implementa o astfel de logică simetrică. Dacă vă place ideea, considerați-o ca un exercițiu pentru dvs. De asemenea, gândiți-vă la o metodă privată mai generică care ar face magia conversiilor automate în ambele cazuri.

Această schimbare complexă a logicii noastre ne obligă să actualizăm încă unul din testele noastre. Ori de câte ori vrem să convertim automat, trebuie să avem un echilibru, chiar dacă este doar zero.

/ ** * Excepție de excepție excepțională * @expectedExceptionMessage Banii scutiți sunt mai mult decât ceea ce avem * / function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Moneda :: EUR ()); $ bani = bani noi (100, Moneda :: EUR ()); // Aceasta este 1 EURO $ acest cont-> depozit ($ bani); $ This-> Cont-> setSecondaryCurrency (valuta :: USD ()); $ money = bani noi (0, valuta :: USD ()); $ This-> Cont-> depozit ($ bani); $ this-> account-> withdraw (bani noi (150, Moneda :: EUR ())); 

Alocarea de bani între conturi

Ultima metodă pe care trebuie să o implementăm Bani este aloca. Aceasta este logica care decide ce trebuie făcut atunci când împărțiți bani între diferite conturi care nu pot fi făcute exact. De exemplu, dacă avem 0,10 cenți și dorim să le alocăm între două conturi într-o proporție de 30-70 de procente, este ușor. Un cont va primi trei cenți, iar ceilalți șapte. Cu toate acestea, dacă vrem să facem aceeași repartizare a raportului de 30-70 de cinci cenți, avem o problemă. Alocarea exactă ar fi de 1,5 cenți într-un cont și 3,5 în cealaltă. Dar nu putem împărți cenți, deci trebuie să implementăm propriul algoritm de alocare a banilor.

Pot exista mai multe soluții la această problemă, un algoritm comun este de a adăuga câte un cent secvențial fiecărui cont. Dacă un cont are mai mulți cenți decât valoarea matematică exactă, acesta trebuie eliminat din lista de alocare și să nu mai primească bani. Iată o reprezentare grafică.


Și un test pentru a ne dovedi punctul nostru de vedere este mai jos.

funcția testItCanAllocateMoney între2Accounturi () $ a1 = $ this-> anAccount (); $ a2 = $ this-> anAccount (); $ money = bani noi (5, Moneda :: USD ()); $ bani-> aloca ($ a1, $ a2, 30, 70); $ this-> assertEquals (bani noi (2, valuta :: USD ()), $ a1-> getPrimaryBalance ()); $ this-> assertEquals (bani noi (3, valuta :: USD ()), $ a2-> getPrimaryBalance ());  funcția privată anAccount () $ account = cont nou (1); $ Cont-> setPrimaryCurrency (valuta :: USD ()); $ account-> deposit (bani noi (0, valuta :: USD ())); returnați contul $; 

Creăm doar o Bani obiect cu cinci cenți și două conturi. Noi sunam aloca și așteptăm ca cele două-trei valori să fie în cele două conturi. De asemenea, am creat o metodă de ajutor pentru a crea rapid conturi. Testul eșuează, așa cum era de așteptat, dar putem trece destul de ușor.

alocarea funcției (cont $ a1, cont $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> suma * $ a1Percent / 100; $ exactA2Balance = $ this-> suma * $ a2Percent / 100; $ oneCent = bani noi (1, $ this-> currency); în timp ce ($ this-> amount> 0) if ($ a1-> getPrimaryBalance () -> getAmount () < $exactA1Balance)  $a1->depozit ($ oneCent); $ This-> amount--;  dacă ($ this-> amount <= 0) break; if ($a2->getPrimaryBalance () -> getAmount () < $exactA2Balance)  $a2->depozit ($ oneCent); $ This-> amount--; 

Ei bine, nu cel mai simplu cod, dar funcționează corect, deoarece trecerea testului nostru o dovedește. Singurul lucru pe care îl putem face în continuare pentru acest cod este de a reduce redundanța redusă din interiorul in timp ce buclă.

alocarea funcției (cont $ a1, cont $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> suma * $ a1Percent / 100; $ exactA2Balance = $ this-> suma * $ a2Percent / 100; în timp ce ($ this-> amount> 0) $ this-> allocateTo ($ a1, $ exactA1Balance); dacă ($ this-> amount <= 0) break; $this->alocaTo ($ a2, $ exactA2Balance);  funcția privată allocateTo ($ cont, $ exactBalance) if ($ account-> getPrimaryBalance () -> getAmount () < $exactBalance)  $account->depozit (bani noi (1, $ this-> currency)); $ This-> amount--; 

Gândurile finale

Ceea ce mi se pare uimitor cu acest mic model este gama largă de cazuri în care o putem aplica.

Am terminat modelul nostru de bani. Am văzut că este un model simplu, care încapsulează specificul conceptului de bani. De asemenea, am văzut că această încapsulare ușurează povara calculărilor din cont. Contul se poate concentra pe reprezentarea conceptului de la un nivel superior, din punctul de vedere al băncii. Contul poate implementa metode cum ar fi conexiunea cu titularii de cont, ID-urile, tranzacțiile și banii. Va fi un orchestrator, nu un calculator. Banii vor avea grijă de calcule.

Ceea ce mi se pare uimitor cu acest mic model este gama largă de cazuri în care o putem aplica. Practic, de fiecare dată când aveți o pereche de unități de valoare, o puteți utiliza. Imaginați-vă că aveți o aplicație meteorologică și doriți să implementați o reprezentare pentru temperatură. Asta ar fi echivalentul obiectului nostru de bani. Puteți folosi valutele Fahrenheit sau Celsius.

Un alt caz de utilizare este atunci când aveți o aplicație de cartografiere și doriți să reprezentați distanțele dintre puncte. Puteți utiliza cu ușurință acest model pentru a comuta între măsurători metrice sau imperiale. Când lucrați cu unități simple, puteți să abandonați obiectul Exchange și să implementați logica simplă de conversie în interiorul obiectului "Money".

Deci, sper că v-ați bucurat de acest tutorial și sunt dornic să aud despre diferitele modalități prin care puteți folosi acest concept. Mulțumesc că ați citit.

Cod