Geldpatroon de juiste manier om waarde-eenheidsparen te vertegenwoordigen

Het geldpatroon, gedefinieerd door Martin Fowler en gepubliceerd in Patronen van Enterprise Application Architecture, is een geweldige manier om paren van waarde-eenheden te vertegenwoordigen. Het wordt Money Pattern genoemd omdat het in een financiële context is ontstaan ​​en we zullen het gebruik ervan voornamelijk in deze context illustreren met behulp van PHP.


Een PayPal-leuk account

Ik heb geen idee hoe PayPal geïmplementeerd is, maar ik denk dat het een goed idee is om de functionaliteit als voorbeeld te nemen. Laat me u laten zien wat ik bedoel, mijn PayPal-rekening heeft twee valuta's: Amerikaanse dollars en Euro. Het houdt de twee waarden gescheiden, maar ik kan geld in elke valuta ontvangen, ik kan mijn totale bedrag zien in een van de twee valuta's en ik kan uitpakken in een van de twee. Stel je voor dit voorbeeld voor dat we uitpakken in een van de valuta's en automatische conversie wordt gedaan als het saldo van die specifieke valuta minder is dan wat we willen overboeken, maar toch is er nog steeds genoeg geld in de andere valuta. We zullen het voorbeeld ook beperken tot slechts twee valuta's.


Een account krijgen

Als ik een accountobject zou maken en gebruiken, zou ik het willen initialiseren met een accountnummer.

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

Dit zal natuurlijk mislukken omdat we nog geen accountklasse hebben.

class Account 

Wel, dat schrijven in een nieuw "Account.php" bestand en het nodig hebben in de test, is het geslaagd. Dit wordt echter allemaal gedaan om onszelf comfortabel te maken met het idee. Vervolgens denk ik aan het krijgen van de accounts ID kaart.

function testItCanCrateANewAccountWithId () $ this-> assertEquals (123, (nieuwe account (123)) -> getId ()); 

Ik heb de vorige test eigenlijk in deze veranderd. Er is geen reden om de eerste te behouden. Het leefde zijn leven, wat betekende dat het me dwong na te denken over de Account klasse en maak het eigenlijk. We kunnen nu verder gaan.

class Account private $ id; function __construct ($ id) $ this-> id = $ id;  openbare functie getId () return $ this-> id; 

De test is voorbij en Account begint er uit te zien als een echte klasse.


valuta

Op basis van onze PayPal-analogie, willen we misschien een primaire en een secundaire valuta voor onze account definiëren.

privé $ account; beschermde functie setUp () $ this-> account = nieuwe account (123);  [...] function testItCanHavePrimaryAndSecondaryCurrencies () $ this-> account-> setPrimaryCurrency ("EUR"); $ This-> account-> setSecondaryCurrency ( 'USD'); $ this-> assertEquals (array ('primary' => 'EUR', 'secondary' => 'USD'), $ this-> account-> getCurrencies ()); 

Nu zal de bovenstaande test ons dwingen de volgende code te schrijven.

class Account private $ id; private $ primaryCurrency; private $ secondaryCurrency; [...] function setPrimaryCurrency ($ currency) $ this-> primaryCurrency = $ currency;  function setSecondaryCurrency ($ currency) $ this-> secondaryCurrency = $ currency;  function getCurrencies () return array ('primary' => $ this-> primaryCurrency, 'secondary' => $ this-> secondaryCurrency); 

Voorlopig houden we valuta als een eenvoudige reeks. Dit kan in de toekomst veranderen, maar we zijn er nog niet.


Geef me het geld

Er zijn eindeloze redenen waarom geld niet als een eenvoudige waarde wordt weergegeven. Drijvende-kommaberekeningen Iedereen? Hoe zit het met valutafracties? Moeten we 10, 100 of 1000 cent hebben in een exotische valuta? Welnu, dit is een ander probleem dat we moeten vermijden. Hoe zit het met het verdelen van ondeelbare centen?

Er zijn gewoon te veel en exotische problemen bij het werken met geld om ze op te schrijven in code, dus we gaan direct verder met de oplossing, het geldpatroon. Dit is een vrij eenvoudig patroon, met grote voordelen en veel gebruik, ver van het financiële domein. Wanneer u een waarde-eenheidspaar moet vertegenwoordigen, zou u dit patroon waarschijnlijk moeten gebruiken.


Het geldpatroon is in feite een klasse die een bedrag en valuta inkapselt. Vervolgens definieert het alle wiskundige bewerkingen op de waarde ten opzichte van de valuta. "toewijzen()" is een speciale functie om een ​​bepaald geldbedrag te verdelen over twee of meer ontvangers.

Dus als gebruiker van Geld Ik zou dit graag in een test willen kunnen doen:

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

Maar dat zal nog niet werken. We hebben beide nodig Geld en Valuta. Sterker nog, we hebben het nodig Valuta voor Geld. Dit zal een eenvoudige les zijn, dus ik zal het voorlopig overslaan. Ik ben er vrij zeker van dat de IDE de meeste code voor mij kan genereren.

class Currency private $ centFactor; private $ stringRepresentation; persoonlijke functie __construct ($ centFactor, $ stringRepresentation) $ this-> centFactor = $ centFactor; $ this-> stringRepresentation = $ stringRepresentation;  openbare functie getCentFactor () return $ this-> centFactor;  functie getStringRepresentation () return $ this-> stringRepresentation;  static function USD () return new self (100, 'USD');  static function EUR () return new self (100, 'EUR'); 

Dat is genoeg voor ons voorbeeld. We hebben twee statische functies voor valuta's in USD en EUR. In een echte toepassing zouden we waarschijnlijk een algemene constructor met een parameter hebben en alle valuta laden vanuit een databasetabel of, nog beter, vanuit een tekstbestand.

Voeg vervolgens de twee nieuwe bestanden in de test toe:

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

Deze test mislukt nog steeds, maar hij kan deze in ieder geval vinden Valuta nu. We gaan verder met een minimum Geld implementatie. Iets meer dan wat deze test strikt vereist, omdat het, wederom, meestal automatisch gegenereerde code is.

klasse Money private $ amount; privé $ valuta; function __construct ($ amount, Currency $ currency) $ this-> amount = $ amount; $ this-> currency = $ currency; 

Merk op dat we het type afdwingen Valuta voor de tweede parameter in onze constructor. Dit is een leuke manier om te voorkomen dat onze klanten rommel als valuta verzenden.


Geld vergelijken

Het eerste dat in me opkwam nadat ik het minimale object in gebruik had, was dat ik op de een of andere manier geldobjecten moest vergelijken. Toen herinnerde ik me dat PHP behoorlijk slim is als het gaat om het vergelijken van objecten, dus ik schreef deze test.

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

Wel, dat gaat echt voorbij. De "AssertEquals" functie kan de twee objecten en zelfs de ingebouwde gelijkheidsvoorwaarde van PHP vergelijken "==" vertelt me ​​wat ik verwacht. Leuk.

Maar hoe zit het als we geïnteresseerd zijn in de ene groter dan de andere? Tot mijn grote verbazing passeert de volgende test ook zonder problemen.

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

Wat ons leidt naar ...

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

... een test die onmiddellijk verloopt.


Plus, Minus, Vermenigvuldigen

Omdat ik zoveel PHP-magie zag werken met vergelijkingen, kon ik het niet laten om deze te proberen.

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

Welke faalt en zegt:

Object van klasse Money kan niet worden geconverteerd naar int

Hmm. Dat klinkt vrij duidelijk. Op dit punt moeten we een beslissing nemen. Het is mogelijk om deze oefening voort te zetten met nog meer PHP-magie, maar deze benadering zal op een gegeven moment deze tutorial omzetten in een PHP-cheat-sheet in plaats van een ontwerppatroon. Laten we dus de beslissing nemen om de feitelijke methoden voor het toevoegen, aftrekken en vermenigvuldigen van geldobjecten te implementeren.

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

Deze test mislukt ook, maar met een fout die ons zegt dat er geen is "toevoegen" methode op Geld.

openbare functie getAmount () return $ this-> amount;  functie add ($ other) retourneer nieuw geld ($ this-> amount + $ other-> getAmount (), $ this-> currency); 

Om er twee samen te vatten Geld objecten, we hebben een manier nodig om de hoeveelheid object op te halen die we doorgeven als het argument. Ik geef de voorkeur aan het schrijven van een getter, maar het classificeren van de klassenvariabele is ook een acceptabele oplossing. Maar wat als we dollars willen toevoegen aan euro's??

/ ** * @expectedException Exception * @expectedExceptionMessage Beide gelden moeten van dezelfde valuta zijn * / function testItThrowsExceptionIfWeTryToAddTwoMoneysWithDifferentCurrency () $ m1 = new Money (100, Currency :: USD ()); $ m2 = nieuw geld (100, valuta :: EUR ()); $ M1-> toe te voegen ($ m2); 

Er zijn verschillende manieren om met operaties om te gaan Geld objecten met verschillende valuta's. We zullen een uitzondering geven en verwachten het in de test. Als alternatief kunnen we een valutaconversiemechanisme in onze toepassing implementeren, het noemen, beide omzetten Geld objecten in een standaardvaluta en vergelijk ze. Of, als we een meer geavanceerd algoritme voor valutaconversie zouden hebben, zouden we altijd van de ene naar de andere kunnen converteren en die geconverteerde valuta kunnen vergelijken. Het punt is dat wanneer conversie plaatsvindt, conversiekosten in aanmerking moeten worden genomen en de zaken behoorlijk gecompliceerd zullen worden. Dus laten we gewoon die uitzondering gooien en verder gaan.

openbare functie getCurrency () return $ this-> currency;  function add (Money $ other) $ this-> sureSameCurrencyWith ($ other); retourneer nieuw geld ($ this-> amount + $ other-> getAmount (), $ this-> currency);  private functie zorgenSameCurrencyWith (Money $ other) if ($ this-> currency! = $ other-> getCurrency ()) gooit een nieuwe uitzondering ("Beide gelden moeten van dezelfde valuta zijn"); 

Dat is beter. We doen een controle om te zien of de valuta's anders zijn en een uitzondering veroorzaken. Ik schreef het al als een afzonderlijke privémethode, omdat ik weet dat we het ook bij de andere wiskundige bewerkingen nodig zullen hebben.

Aftrekken en vermenigvuldigen lijken sterk op toevoegen, dus hier is de code en u kunt de tests vinden in de bijgevoegde broncode.

functie aftrekken (Money $ ander) $ this-> sureSameCurrencyWith ($ other); als ($ anders> $ dit) nieuwe uitzondering gooit ("Afgetrokken geld is meer dan wat we hebben"); retourneer nieuw geld ($ this-> amount - $ other-> getAmount (), $ this-> currency);  functie multiplyBy ($ multiplier, $ roundMethod = PHP_ROUND_HALF_UP) $ product = round ($ this-> amount * $ multiplier, 0, $ roundMethod); retourneer nieuw geld ($ product, $ dit-> valuta); 

Met aftrekken moeten we ervoor zorgen dat we genoeg geld hebben en met vermenigvuldiging moeten we actie ondernemen om de dingen op of af te ronden, zodat verdeling (vermenigvuldiging met aantallen minder dan één) geen "halve cent" oplevert. We houden ons bedrag in centen, de laagst mogelijke factor van de valuta. We kunnen het niet meer verdelen.


Introductie van valuta op onze account

We hebben een bijna voltooid Geld en Valuta. Het is tijd om deze objecten te introduceren Account. We zullen beginnen met Valuta, en onze testen dienovereenkomstig veranderen.

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

Vanwege het dynamisch typerende karakter van PHP, gaat deze test zonder problemen over. Ik zou de methoden echter graag willen forceren Account gebruiken Valuta objecten en accepteren niets anders. Dit is niet verplicht, maar ik vind dit soort typefeestjes erg handig wanneer iemand anders onze code moet begrijpen.

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

Nu is het voor iedereen die deze code leest voor de hand liggend Account werkt met Valuta.


Introductie van geld op onze rekening

De twee standaardacties die een account moet uitvoeren, zijn: storten - dit betekent geld toevoegen aan een account - en opnemen - wat betekent dat u geld van een account verwijdert. Storten heeft een bron en opnemen heeft een andere bestemming dan onze huidige account. We zullen niet ingaan op details over hoe deze transacties kunnen worden geïmplementeerd, we zullen ons alleen concentreren op het implementeren van de effecten die deze hebben op onze account. We kunnen ons dus een test als deze voorstellen voor het storten.

function testAccountCanDepositMoney () $ this-> account-> setPrimaryCurrency (Currency: EUR ()); $ geld = nieuw geld (100, valuta :: EUR ()); // Dat is 1 EURO $ dit-> rekening-> storting ($ geld); $ this-> assertEquals ($ money, $ this-> account-> getPrimaryBalance ()); 

Dit zal ons dwingen om vrij veel implementatiecode te schrijven.

class Account private $ id; private $ primaryCurrency; private $ secondaryCurrency; privé $ secondaryBalance; private $ primaryBalance; functie getSecondaryBalance () return $ this-> secondaryBalance;  functie getPrimaryBalance () return $ this-> primaryBalance;  function __construct ($ id) $ this-> id = $ id;  [...] functie storting (Money $ money) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ money: $ this-> secondaryBalance = $ money; 

OKE OKE. Ik weet het, ik schreef meer dan wat absoluut noodzakelijk was, voor productie. Maar ik wil je niet dood boren met baby-steps en ik ben ook vrij zeker van de code voor secondaryBalance zal correct werken. Het werd bijna volledig gegenereerd door de IDE. Ik zal het zelfs overslaan. Hoewel deze code onze test doet slagen, moeten we ons afvragen wat er gebeurt als we volgende stortingen doen? We willen dat ons geld wordt toegevoegd aan het vorige saldo.

function testSubsequentDepositsAddUpTheMoney () $ this-> account-> setPrimaryCurrency (Currency: EUR ()); $ geld = nieuw geld (100, valuta :: EUR ()); // Dat is 1 EURO $ dit-> rekening-> storting ($ geld); // Een euro op de rekening $ this-> account-> storting ($ geld); // Twee euro in de account $ this-> assertEquals ($ money-> multiplyBy (2), $ this-> account-> getPrimaryBalance ()); 

Wel, dat mislukt. Dus we moeten onze productiecode updaten.

function deposit (Money $ money) if ($ this-> primaryCurrency == $ money-> getCurrency ()) $ this-> primaryBalance = $ this-> primaryBalance? : nieuw geld (0, $ dit-> primaire valuta); $ this-> primaryBalance = $ this-> primaryBalance-> add ($ money);  else $ this-> secondaryBalance = $ this-> secondaryBalance? : nieuw geld (0, $ dit-> secondaryCurrency); $ this-> secondaryBalance = $ this-> secondaryBalance-> add ($ money); 

Dit is veel beter. We zijn waarschijnlijk klaar met de storting methode en we kunnen doorgaan terugtrekken.

function testAccountCanWithdrawMoneyOfSameCurrency () $ this-> account-> setPrimaryCurrency (Currency: EUR ()); $ geld = nieuw geld (100, valuta :: EUR ()); // Dat is 1 EURO $ dit-> rekening-> storting ($ geld); $ this-> account-> op te nemen (nieuw geld (70, valuta :: EUR ())); $ this-> assertEquals (nieuw geld (30, valuta :: EUR ()), $ this-> account-> getPrimaryBalance ()); 

Dit is slechts een eenvoudige test. De oplossing is ook eenvoudig.

functie terugtrekken (Money $ money) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> aftrekken ($ geld): $ this-> secondaryBalance = $ this-> secondaryBalance-> aftrekken ($ geld); 

Wel, dat werkt, maar wat als we een a willen gebruiken? Valuta dat staat niet op onze account? Daar moeten we een Excpetion voor gooien.

/ ** * @expectedException Exception * @expectedExceptionMessage Dit account heeft geen valuta. USD * / function testThrowsExceptionForInexistentCurrencyOnWithdraw () $ this-> account-> setPrimaryCurrency (Currency: EUR ()); $ geld = nieuw geld (100, valuta :: EUR ()); // Dat is 1 EURO $ dit-> rekening-> storting ($ geld); $ this-> account-> op te nemen (nieuw geld (70, valuta :: USD ())); 

Dat zal ons ook dwingen onze valuta's te controleren.

functie intrekken (Money $ money) $ this-> validateCurrencyFor ($ money); $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> aftrekken ($ geld): $ this-> secondaryBalance = $ this-> secondaryBalance-> aftrekken ($ geld);  private function validateCurrencyFor (Money $ money) if (! in_array ($ money-> getCurrency (), $ this-> getCurrencies ())) gooi nieuwe Exception (sprintf ('Dit account heeft geen valuta% s', $ money -> getCurrency () -> getStringRepresentation ())); 

Maar wat als we meer willen opnemen dan wat we hebben? Die zaak was al behandeld toen we aftrekken implementeerden Geld. Hier is de test die het bewijst.

/ ** * @expectedException Exception * @expectedExceptionMessage Afgetrokken geld is meer dan wat we hebben * / function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Currency: EUR ()); $ geld = nieuw geld (100, valuta :: EUR ()); // Dat is 1 EURO $ dit-> rekening-> storting ($ geld); $ this-> account-> op te nemen (nieuw geld (150, valuta :: EUR ())); 

Omgaan met opnemen en omruilen

Een van de moeilijkere dingen om mee om te gaan als we met meerdere valuta's werken, is uitwisseling tussen hen. Het mooie van dit ontwerppatroon is dat het ons in staat stelt dit probleem enigszins te vereenvoudigen door het in zijn eigen klasse te isoleren en in te kapselen. Terwijl de logica in een Uitwisseling klas kan heel geavanceerd zijn, het gebruik ervan wordt veel eenvoudiger. Laten we ons in het belang van deze tutorial voorstellen dat we een aantal heel basaal zijn Uitwisseling alleen logica. 1 EUR = 1,5 USD.

class Exchange functie converteren (Money $ money, Currency $ toCurrency) if ($ toCurrency == Currency :: EUR () && $ money-> getCurrency () == Valuta: USD ()) retourneert nieuw geld ($ geld -> multiplyBy (0.67) -> getAmount (), $ toCurrency); if ($ toCurrency == Currency :: USD () && $ money-> getCurrency () == Currency :: EUR ()) levert nieuw geld op ($ money-> multiplyBy (1.5) -> getAmount (), $ toCurrency) ; $ geld teruggeven; 

Als we van EUR naar USD converteren, vermenigvuldigen we de waarde met 1,5, als we de waarde van USD naar EUR converteren, verdelen we de waarde met 1,5, anders veronderstellen we dat we twee valuta van hetzelfde type converteren, dus we doen niets en geven gewoon het geld terug . Natuurlijk zou dit in werkelijkheid een veel gecompliceerdere klasse zijn.

Nu, met een Uitwisseling klasse, Account kan verschillende beslissingen nemen wanneer we ons willen terugtrekken Geld in een valuta, maar we verhogen niet genoeg in die specifieke valuta. Hier is een test die dit beter illustreert.

function testItConvertsMoneyFromTheOtherCurrencyWhenWeDoNotHaveEnoughInTheCurrentOne () $ this-> account-> setPrimaryCurrency (Currency :: USD ()); $ geld = nieuw geld (100, valuta :: USD ()); // Dat is 1 USD $ dit-> rekening-> aanbetaling ($ geld); $ This-> account-> setSecondaryCurrency (Currency :: EUR ()); $ geld = nieuw geld (100, valuta :: EUR ()); // Dat is 1 EURO = 1,5 USD $ dit-> rekening-> storting ($ geld); $ this-> account-> op te nemen (nieuw geld (200, valuta :: USD ())); // Dat is 2 USD $ dit-> assertEquals (nieuw Money (0, Currency :: USD ()), $ this-> account-> getPrimaryBalance ()); $ this-> assertEquals (nieuw geld (34, valuta :: EUR ()), $ dit-> account-> getSecondaryBalance ()); 

We hebben de primaire valuta van onze rekening ingesteld op USD en één dollar gestort. Vervolgens stellen we de secundaire valuta in op EUR en storten we één euro. Dan trekken we twee dollars terug. Ten slotte verwachten we dat we met nul dollar en 0,34 euro zullen blijven. Natuurlijk werpt deze test een uitzondering, dus we moeten een oplossing voor dit dilemma implementeren.

functie intrekken (Money $ money) $ this-> validateCurrencyFor ($ money); if ($ this-> primaryCurrency == $ money-> getCurrency ()) if ($ this-> primaryBalance> = $ money) $ this-> primaryBalance = $ this-> primaryBalance-> subtract ($ money);  else $ ourMoney = $ this-> primaryBalance-> add ($ this-> secondaryToPrimary ()); $ remainingMoney = $ ourMoney-> aftrekken ($ geld); $ this-> primaryBalance = new Money (0, $ this-> primaryCurrency); $ this-> secondaryBalance = (nieuwe Exchange ()) -> convert ($ remainingMoney, $ this-> secondaryCurrency);  else $ this-> secondaryBalance = $ this-> secondaryBalance-> aftrekken ($ geld);  private functie secondaryToPrimary () return (new Exchange ()) -> convert ($ this-> secondaryBalance, $ this-> primaryCurrency); 

Wow, er moesten veel veranderingen worden doorgevoerd om deze automatische conversie te ondersteunen. Wat er gebeurt, is dat als we in het geval van extractie uit onze primaire valuta zijn en we niet genoeg geld hebben, we ons saldo van de secundaire valuta in primair omzetten en de aftrekking opnieuw proberen. Als we nog steeds niet genoeg geld hebben, is de $ ourMoney object gooit de juiste uitzondering. Anders stellen we ons primaire saldo op nul en converteren we het resterende geld terug naar de secundaire valuta en stellen we ons secundaire saldo in op die waarde.

Het blijft aan de logica van onze account om een ​​vergelijkbare automatische conversie voor secundaire valuta te implementeren. We zullen zo'n symmetrische logica niet implementeren. Als je het idee leuk vindt, beschouw het dan als een oefening voor jou. Denk ook aan een meer generieke privémethode die de magie van automatische conversies in beide gevallen zou doen.

Deze complexe verandering in onze logica dwingt ons ook om een ​​andere van onze tests te updaten. Wanneer we automatisch willen converteren, moeten we een balans hebben, zelfs als deze slechts nul is.

/ ** * @expectedException Exception * @expectedExceptionMessage Afgetrokken geld is meer dan wat we hebben * / function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Currency: EUR ()); $ geld = nieuw geld (100, valuta :: EUR ()); // Dat is 1 EURO $ dit-> rekening-> storting ($ geld); $ This-> account-> setSecondaryCurrency (Currency :: USD ()); $ geld = nieuw geld (0, valuta :: USD ()); $ This-> account-> borg ($ geld); $ this-> account-> op te nemen (nieuw geld (150, valuta :: EUR ())); 

Geld toewijzen tussen accounts

De laatste methode die we moeten implementeren Geld is toewijzen. Dit is de logica die beslist wat te doen bij het verdelen van geld tussen verschillende accounts die niet precies gemaakt kunnen worden. Als we bijvoorbeeld 0,10 cent hebben en we willen ze toewijzen tussen twee accounts in een verhouding van 30-70 procent, dan is dat eenvoudig. Eén account krijgt drie cent en de andere zeven. Als we echter dezelfde 30-70-verhoudingstoewijzing van vijf cent willen maken, hebben we een probleem. De exacte toewijzing zou 1,5 cent op de ene en 3,5 op de andere zijn. Maar we kunnen geen cent verdelen, dus we moeten ons eigen algoritme implementeren om het geld toe te wijzen.

Er kunnen verschillende oplossingen voor dit probleem zijn, een algemeen algoritme is om één cent sequentieel aan elk account toe te voegen. Als een account meer centen heeft dan de exacte wiskundige waarde, moet het worden geëlimineerd van de toewijzingslijst en geen geld meer ontvangen. Hier is een grafische weergave.


En een test om te bewijzen dat ons punt hieronder is.

function testItCanAllocateMoneyBetween2Accounts () $ a1 = $ this-> anAccount (); $ a2 = $ this-> anAccount (); $ geld = nieuw geld (5, valuta :: USD ()); $ geld-> toewijzen ($ a1, $ a2, 30, 70); $ this-> assertEquals (nieuw geld (2, valuta :: USD ()), $ a1-> getPrimaryBalance ()); $ this-> assertEquals (new Money (3, Currency :: USD ()), $ a2-> getPrimaryBalance ());  private function anAccount () $ account = nieuwe account (1); $ Account-> setPrimaryCurrency (Currency :: USD ()); $ account-> storting (nieuw geld (0, valuta :: USD ())); $ account retourneren; 

We maken gewoon een Geld object met vijf cent en twee accounts. Wij bellen toewijzen en verwacht dat de twee tot drie waarden in de twee accounts staan. We hebben ook een hulpmethode gemaakt om snel accounts aan te maken. De test mislukt, zoals verwacht, maar we kunnen het vrij gemakkelijk laten passeren.

functie alloceren (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->borg ($ oneCent); $ This-> amount--;  if ($ this-> amount <= 0) break; if ($a2->getPrimaryBalance () -> getAmount () < $exactA2Balance)  $a2->borg ($ oneCent); $ This-> amount--; 

Welnu, niet de eenvoudigste code, maar het werkt correct, omdat het slagen van onze test het bewijst. Het enige dat we nog kunnen doen met deze code is het verkleinen van de kleine duplicatie in de terwijl lus.

functie alloceren (Account $ a1, Account $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> amount * $ a1Percent / 100; $ exactA2Balance = $ this-> amount * $ a2Percent / 100; while ($ this-> amount> 0) $ this-> allocateTo ($ a1, $ exactA1Balance); if ($ this-> amount <= 0) break; $this->allocateTo ($ a2, $ exactA2Balance);  persoonlijke functie allocateTo ($ account, $ exactBalance) if ($ account-> getPrimaryBalance () -> getAmount () < $exactBalance)  $account->storting (nieuw geld (1, $ dit-> valuta)); $ This-> amount--; 

Laatste gedachten

Wat ik verbazingwekkend vind met dit kleine patroon is het grote aantal gevallen waarin we het kunnen toepassen.

We zijn klaar met ons geldpatroon. We zagen dat het een vrij eenvoudig patroon is, dat de details van het geldconcept inkapselt. We zagen ook dat deze inkapseling de last van berekeningen van Account verlicht. Account kan zich concentreren op het vertegenwoordigen van het concept van een hoger niveau, vanuit het oogpunt van de bank. Account kan methoden implementeren zoals verbinding met accounthouders, ID's, transacties en geld. Het zal een orkestrator zijn, geen rekenmachine. Geld zal voor berekeningen zorgen.

Wat ik verbazingwekkend vind met dit kleine patroon is het grote aantal gevallen waarin we het kunnen toepassen. Kortom, elke keer dat u een waarde-eenheidspaar hebt, kunt u het gebruiken. Stel je voor dat je een weerapplicatie hebt en dat je een representatie voor temperatuur wilt implementeren. Dat zou het equivalent zijn van ons Money-object. U kunt Fahrenheit of Celsius als valuta gebruiken.

Een ander geval van gebruik is wanneer u een toewijzingstoepassing hebt en u afstanden tussen punten wilt weergeven. U kunt dit patroon eenvoudig gebruiken om te schakelen tussen metrische of imperiale metingen. Wanneer u met eenvoudige eenheden werkt, kunt u het Exchange-object laten vallen en de eenvoudige conversielogica in uw "Geld" -object implementeren.

Ik hoop dat je deze tutorial leuk vond en ik ben benieuwd naar de verschillende manieren waarop je dit concept zou kunnen gebruiken. Bedankt voor het lezen.