Pengemönster Det rätta sättet att representera värdeparametrar

Pengemönstret, definierat av Martin Fowler och publicerat i Mönster av Enterprise Application Architecture, är ett utmärkt sätt att representera värdenhetspar. Det kallas Money Pattern eftersom det uppstod i ett finansiellt sammanhang och vi kommer att illustrera dess användning huvudsakligen i det här sammanhanget med PHP.


Ett PayPal Liksom konto

Jag har ingen aning om hur PayPal implementeras, men jag tycker att det är en bra idé att ta dess funktionalitet som ett exempel. Låt mig visa dig vad jag menar, mitt PayPal-konto har två valutor: amerikanska dollar och euro. Det håller de två värdena separerade, men jag kan få pengar i vilken valuta som helst, jag kan se mitt totala belopp i någon av de två valutorna och jag kan extrahera i någon av de två. För det här exemplets skull, tänk oss att vi extraherar i någon av valutorna och automatisk omvandling görs om balansen i den specifika valutan är mindre än vad vi vill överföra men det finns fortfarande tillräckligt med pengar i den andra valutan. Vi kommer också att begränsa exemplet till endast två valutor.


Få ett konto

Om jag skulle skapa och använda ett kontoobjekt vill jag initiera det med ett kontonummer.

funktion testItCanCrateANewAccount () $ this-> assertInstanceOf ("Konto", nytt konto (123)); 

Detta kommer naturligtvis att misslyckas eftersom vi inte har någon konto klass, än.

klasskonto 

Tja, skriv det i en ny "Account.php" fil och krävde det i testet, fick det att passera. Men allting görs bara för att göra oss bekanta med idén. Därefter tänker jag på att få kontot id.

funktionstestItCanCrateANewAccountWithId () $ this-> assertEquals (123, (nytt konto (123)) -> getId ()); 

Jag har faktiskt ändrat det tidigare testet i den här. Det finns ingen anledning att behålla den första. Det levde det är livet, vilket innebar att det tvingade mig att tänka på konto klass och skapa faktiskt det. Vi kan nu gå vidare.

klasskonto privat $ id; funktion __construct ($ id) $ this-> id = $ id;  allmän funktion getId () return $ this-> id; 

Testet passerar och konto börjar se ut som en riktig klass.


Valutor

Baserat på vår PayPal-analogi kanske vi vill definiera en primär och en sekundär valuta för vårt konto.

privat $ konto skyddad funktion setUp () $ this-> account = nytt konto (123);  [...] funktionstestItCanHavePrimaryAndSecondaryCurrencies () $ this-> account-> setPrimaryCurrency ("EUR"); $ This-> konto> setSecondaryCurrency (USD); $ this-> assertEquals (array ('primary' => 'EUR', 'secondary' => 'USD'), $ this-> account-> getCurrencies ()); 

Nu kommer ovanstående test att tvinga oss att skriva följande kod.

klasskonto privat $ id; privat $ primaryCurrency; privat $ secondaryCurrency; [...] funktion setPrimaryCurrency ($ valuta) $ this-> primaryCurrency = $ currency;  funktion setSecondaryCurrency ($ valuta) $ this-> secondaryCurrency = $ currency;  funktion getCurrencies () return array ('primary' => $ this-> primaryCurrency, 'secondary' => $ this-> secondaryCurrency); 

För närvarande håller vi valutan som en enkel sträng. Det kan komma att ändras i framtiden, men vi är inte där ännu.


Gimme pengarna

Det finns oändliga skäl att inte representera pengar som ett enkelt värde. Floating point beräkningar? Någon? Vad sägs om valutafraktioner? Ska vi ha 10, 100 eller 1000 cent i någon exotisk valuta? Tja, det här är ett annat problem som vi måste undvika. Vad sägs om att fördela odelbara cent?

Det finns bara för många och exotiska problem när man arbetar med pengar för att skriva ner dem i kod, så vi går direkt till lösningen, pengemönstret. Det här är ganska enkelt, med stora fördelar och många användarfall, långt ifrån den finansiella domänen. När du måste representera ett värdeparpar ska du förmodligen använda det här mönstret.


Pengemönstret är i grunden en klass som inkapslar en mängd och valuta. Därefter definieras all matematisk verksamhet på värdet i förhållande till valutan. "allokera()" är en speciell funktion för att fördela en viss summa pengar mellan två eller flera mottagare.

Så, som användare av Pengar Jag skulle vilja kunna göra detta i ett test:

klass MoneyTest utökar PHPUnit_Framework_TestCase funktion testWeCanCreateAMoneyObject () $ money = nya pengar (100, valuta :: USD ()); 

Men det kommer inte fungera ännu. Vi behöver båda Pengar och Valuta. Ännu mer behöver vi Valuta innan Pengar. Det här blir en enkel klass, så jag kommer att hoppa över testningen för nu. Jag är ganska säker på att IDE kan generera det mesta av koden för mig.

klass Valuta privat $ centFactor; privat $ stringRepresentation; privat funktion __construct ($ centFactor, $ stringRepresentation) $ this-> centFactor = $ centFactor; $ this-> stringRepresentation = $ stringRepresentation;  offentlig funktion getCentFactor () returnera $ this-> centFactor;  funktion getStringRepresentation () return $ this-> stringRepresentation;  statisk funktion USD () returnera nytt själv (100, 'USD');  statisk funktion EUR () returnera nytt själv (100, 'EUR'); 

Det är nog för vårt exempel. Vi har två statiska funktioner för USD och EUR valutor. I en riktig applikation skulle vi förmodligen ha en generell konstruktör med en parameter och ladda alla valutor från en databas tabell eller, ännu bättre, från en textfil.

Följ sedan de två nya filerna i testet:

require_once "... /Currency.php '; require_once "... /Money.php '; klass MoneyTest utökar PHPUnit_Framework_TestCase funktion testWeCanCreateAMoneyObject () $ money = nya pengar (100, valuta :: USD ()); 

Detta test misslyckas fortfarande, men det kan åtminstone hittas Valuta nu. Vi fortsätter med en minimal Pengar genomförande. Lite mer än vad detta test strikt kräver eftersom det är igen, mestadels automatiskt genererad kod.

klass pengar privat $ belopp; privat $ valuta; funktion __construct ($ summa, Valuta $ valuta) $ this-> amount = $ amount; $ this-> currency = $ currency; 

Observera, vi verkställer typen Valuta för den andra parametern i vår konstruktör. Det här är ett bra sätt att undvika att våra kunder skickar i skräp som valuta.


Jämförande pengar

Det första som kom in i mitt sinne efter att ha det minsta objektet igång var att jag måste jämföra pengar objekt på något sätt. Då kom jag ihåg att PHP är ganska smart när det gäller att jämföra objekt, så jag skrev detta test.

funktion testItCanTellTwoMoneyObjectAreEqual () $ m1 = nya pengar (100, valuta :: USD ()); $ m2 = nya pengar (100, valuta :: USD ()); $ This-> assertEquals ($ m1, $ m2); $ this-> assertTrue ($ m1 == $ m2); 

Jo, det passerar faktiskt. De "assertEquals" funktionen kan jämföra de två objekten och till och med det inbyggda jämställdhetsvillkoren från PHP "==" berättar för mig vad jag förväntar mig. Trevlig.

Men hur är det om vi är intresserade av att vara större än den andra? Till min ännu större överraskning passerar följande test också utan problem.

funktion testOneMoneyIsBiggerThanTheOther () $ m1 = nya pengar (200, valuta :: USD ()); $ m2 = nya pengar (100, valuta :: USD ()); $ this-> assertGreaterThan ($ m2, $ m1); $ this-> assertTrue ($ m1> $ m2); 

Vilket leder oss till ...

funktion testOneMoneyIsLessThanTheOther () $ m1 = nya pengar (100, valuta :: USD ()); $ m2 = nya pengar (200, valuta :: USD ()); $ this-> assertLessThan ($ m2, $ m1); $ This-> assertTrue ($ M1 < $m2); 

... ett test som passerar omedelbart.


Plus, Minus, Multiplicera

Att se så mycket PHP magi faktiskt jobbar med jämförelser, kunde jag inte motstå för att prova den här.

funktionstestTwoMoneyObjectsCanBeAdded () $ m1 = nya pengar (100, valuta :: USD ()); $ m2 = nya pengar (200, valuta :: USD ()); $ sum = nya pengar (300, valuta :: USD ()); $ this-> assertEquals ($ sum, $ m1 + $ m2); 

Vilket misslyckas och säger:

Objekt av klass Pengar kunde inte konverteras till int

Hmm. Det låter ganska uppenbart. Vid denna tidpunkt måste vi fatta ett beslut. Det är möjligt att fortsätta denna övning med ännu mer PHP-magi, men detta tillvägagångssätt kommer på något sätt att förvandla denna handledning till ett PHP-cheatsheet istället för ett designmönster. Så, låt oss fatta beslutet att genomföra de faktiska metoderna för att lägga till, subtrahera och multiplicera pengarobjekt.

funktionstestTwoMoneyObjectsCanBeAdded () $ m1 = nya pengar (100, valuta :: USD ()); $ m2 = nya pengar (200, valuta :: USD ()); $ sum = nya pengar (300, valuta :: USD ()); $ this-> assertEquals ($ sum, $ m1-> add ($ m2)); 

Det här testet misslyckas också, men med ett fel som berättar för oss finns det ingen "Lägg till" metod på Pengar.

offentlig funktion getAmount () return $ this-> amount;  funktion add ($ other) returnera nya pengar ($ this-> summa + $ other-> getAmount (), $ this-> currency); 

Sammanfattningsvis två Pengar objekt, vi behöver ett sätt att hämta mängden objekt vi passerar in som argumentet. Jag föredrar att skriva en getter, men det är också en acceptabel lösning att ändra klassvariabeln för att vara allmän. Men vad händer om vi vill lägga till dollar till euro?

/ ** * @expectedException Exception * @expectedExceptionMessage Båda pengarna måste vara av samma valuta * / funktion testItThrowsExceptionIfWeTryToAddTwoMoneysWithDifferentCurrency () $ m1 = nya Pengar (100, Valuta :: USD ()); $ m2 = nya pengar (100, valuta :: EUR ()); $ M1> lägga ($ m2); 

Det finns flera sätt att hantera verksamheten på Pengar objekt med olika valutor. Vi kommer att kasta ett undantag och förvänta oss det i testet. Alternativt kan vi implementera en valutaomvandlingsmekanism i vår ansökan, kalla den, konvertera båda Pengar objekt i vissa standardvaluta och jämföra dem. Eller om vi skulle ha en mer sofistikerad valutaomvandlingsalgoritm kunde vi alltid konvertera från en till en annan och jämföra i den konverterade valutan. Saken är att när konvertering kommer på plats måste omräkningsavgifter beaktas och saker blir ganska komplicerade. Så låt oss bara kasta det undantaget och fortsätta.

offentlig funktion getCurrency () return $ this-> currency;  funktion add ($ $ $) $ this-> secureSameCurrencyWith ($ other); returnera nya pengar ($ this-> summa + $ other-> getAmount (), $ this-> currency);  Privat funktion säkerställaSameCurrencyWith (Money $ other) om ($ this-> currency! = $ other-> getCurrency ()) kasta ny Undantag ("båda pengarna måste vara av samma valuta"); 

Det är bättre. Vi gör en check för att se om valutorna är olika och kasta ett undantag. Jag skrev det redan som en separat privat metod, för jag vet att vi kommer att behöva det i de andra matematiska operationerna också.

Subtraktion och multiplikation är mycket likadana tillägg, så här är koden och du kan hitta testen i den bifogade källkoden.

funktion subtrahera (pengar $ annat) $ this-> secureSameCurrencyWith ($ other); om ($ other> $ this) kasta ny Undantag ("Subtraherade pengar är mer än vad vi har"); returnera nya pengar ($ this-> summa - $ other-> getAmount (), $ this-> currency);  funktion multipliceraBy ($ multiplikator, $ roundMethod = PHP_ROUND_HALF_UP) $ product = round ($ this-> mängd * $ multiplikator, 0, $ roundMethod); returnera nya pengar ($ produkt, $ this-> valuta); 

Med subtraktion måste vi se till att vi har tillräckligt med pengar och med multiplikation måste vi vidta åtgärder för att runda saker upp eller ner så att uppdelning (multiplikation med siffror mindre än en) inte kommer att producera "halva cent". Vi håller vårt belopp i cent, den lägsta möjliga faktorn för valutan. Vi kan inte dela upp det mer.


Vi presenterar valuta för vårt konto

Vi har en nästan komplett Pengar och Valuta. Det är dags att introducera dessa objekt till konto. Vi börjar med Valuta, och ändra våra test i enlighet därmed.

funktionstestItCanHavePrimaryAndSecondaryCurrencies () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ This-> konto> setSecondaryCurrency (valuta :: USD ()); $ this-> assertEquals (array ('primary' => Valuta :: EUR (), 'secondary' => Valuta :: USD ()), $ this-> account-> getCurrencies ()); 

På grund av PHPs dynamiska typingskaraktär passerar detta test utan några problem. Men jag skulle vilja tvinga metoderna i konto att använda Valuta objekt och acceptera inte något annat. Det här är inte obligatoriskt, men jag finner dessa typer av anvisningar mycket användbara när någon annan behöver förstå vår kod.

funktion setPrimaryCurrency (Valuta $ valuta) $ this-> primaryCurrency = $ currency;  funktion setSecondaryCurrency (Valuta $ valuta) $ this-> secondaryCurrency = $ currency; 

Nu är det uppenbart för alla att läsa den här koden för första gången konto arbetar med Valuta.


Presentera pengar på vårt konto

De två grundläggande åtgärderna som ett konto måste tillhandahålla är: insättning - vilket innebär att du lägger pengar på ett konto - och dra tillbaka - vilket innebär att du tar bort pengar från ett konto. Deponering har en källa och uttagning har en annan destination än vårt nuvarande konto. Vi kommer inte att gå in på detaljer om hur man genomför dessa transaktioner, vi kommer bara att koncentrera oss på att genomföra de effekter de har på vårt konto. Så vi kan föreställa oss ett test som detta för deponering.

funktion testAccountCanDepositMoney () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nya pengar (100, valuta :: EUR ()); // Det är 1 EURO $ this-> account-> deposition ($ money); $ this-> assertEquals ($ money, $ this-> account-> getPrimaryBalance ()); 

Detta kommer tvinga oss att skriva en hel del implementeringskod.

klasskonto privat $ id; privat $ primaryCurrency; privat $ secondaryCurrency; privat $ secondaryBalance; privat $ primaryBalance; funktion getSecondaryBalance () return $ this-> secondaryBalance;  funktion getPrimaryBalance () returnera $ this-> primaryBalance;  funktionen __construct ($ id) $ this-> id = $ id;  [...] funktionsavgift (pengar $ pengar) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ money: $ this-> secondaryBalance = $ money; 

OKEJ OKEJ. Jag vet att jag skrev mer än vad som var absolut nödvändigt för produktion. Men jag vill inte döda dig med barnsteg och jag är också ganska säker på koden för secondaryBalance kommer att fungera korrekt. Det genererades nästan helt av IDE. Jag kommer även att hoppa över testet. Medan den här koden gör vårt testpass måste vi fråga oss vad som händer när vi gör efterföljande insättningar? Vi vill att våra pengar ska läggas till föregående balans.

funktionstestSubsequentDepositsAddUpTheMoney () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nya pengar (100, valuta :: EUR ()); // Det är 1 EURO $ this-> account-> deposition ($ money); // En euro i kontot $ this-> account-> deposition ($ money); // Två euro i kontot $ this-> assertEquals ($ money-> multiplyBy (2), $ this-> account-> getPrimaryBalance ()); 

Jo det misslyckas. Så vi måste uppdatera vår produktionskod.

($ $ )- $ primary : nya pengar (0, $ this-> primaryCurrency); $ this-> primaryBalance = $ this-> primaryBalance-> add ($ money);  annars $ this-> secondaryBalance = $ this-> secondaryBalance? : nya pengar (0, $ this-> secondaryCurrency); $ this-> secondaryBalance = $ this-> secondaryBalance-> add ($ money); 

Detta är mycket bättre. Vi är noga gjort med deposition metod och vi kan fortsätta med dra tillbaka.

funktion testAccountCanWithdrawMoneyOfSameCurrency () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nya pengar (100, valuta :: EUR ()); // Det är 1 EURO $ this-> account-> deposition ($ money); $ this-> account-> withdraw (nya pengar (70, valuta :: EUR ())); $ this-> assertEquals (nya pengar (30, valuta :: EUR ()), $ this-> account-> getPrimaryBalance ()); 

Detta är bara ett enkelt test. Lösningen är enkel också.

funktionen dra tillbaka (pengar $ pengar) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> subtrahera ($ money): $ this-> secondaryBalance = $ this-> secondaryBalance-> subtrahera ($ pengar); 

Jo, det fungerar, men om vi vill använda en Valuta Det finns inte i vårt konto? Vi borde slänga för det.

/ ** * @expectedException Exception * @expectedExceptionMessage Detta konto har ingen valuta USD * / funktionstestThrowsExceptionForInexistentCurrencyOnWithdraw () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nya pengar (100, valuta :: EUR ()); // Det är 1 EURO $ this-> account-> deposition ($ money); $ this-> account-> withdraw (nya pengar (70, valuta :: USD ())); 

Det kommer också att tvinga oss att kontrollera våra valutor.

funktionen dra tillbaka (pengar $ pengar) $ this-> validateCurrencyFor ($ money); $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> subtrahera ($ money): $ this-> secondaryBalance = $ this-> secondaryBalance-> subtrahera ($ pengar);  privat funktion valideraCurrencyFor (Money $ money) om (! in_array ($ money-> getCurrency (), $ this-> getCurrencies ())) kasta ny Undantag (sprintf ('Det här kontot har ingen valuta% s' -> getCurrency () -> getStringRepresentation ())); 

Men vad händer om vi vill dra tillbaka mer än vad vi har? Det fallet togs redan upp när vi genomförde subtraktion på Pengar. Här är det test som bevisar det.

/ ** * @expectedException Exception * @expectedExceptionMessage Subtraherade pengar är mer än vad vi har * / funktion testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nya pengar (100, valuta :: EUR ()); // Det är 1 EURO $ this-> account-> deposition ($ money); $ this-> account-> withdraw (nya pengar (150, valuta :: EUR ())); 

Hantera uttag och utbyte

En av de svårare sakerna att hantera när vi arbetar med flera valutor byter ut mellan dem. Skönheten i detta designmönster är att det gör det möjligt för oss att förenkla problemet något genom att isolera och inkapsla det i sin egen klass. Medan logiken i en Utbyta klassen kan vara mycket sofistikerad, användningen blir mycket lättare. För tanke på denna handledning, låt oss föreställa oss att vi har några väldigt grundläggande Utbyta logik bara. 1 EUR = 1,5 USD.

klassen Exchange funktion konvertera (pengar $ pengar, valuta $ till valuta) if ($ toCurrency == valuta :: EUR () && $ money-> getCurrency () == Valuta :: USD ()) returnera nya pengar -> multiplyBy (0.67) -> getAmount (), $ till valutan); om ($ toCurrency == Valuta :: USD () && $ money-> getCurrency () == Valuta :: EUR ()) returnera nya pengar ($ money-> multiplyBy (1.5) -> getAmount (), $ till Valuta) ; returnera $ pengar; 

Om vi ​​konverterar från EUR till USD multiplicerar vi värdet med 1,5, om vi konverterar från USD till EUR delar vi värdet med 1,5, annars antar vi att vi konverterar två valutor av samma typ, så vi gör ingenting och bara returnerar pengarna . Naturligtvis skulle det i själva verket vara en mycket mer komplicerad klass.

Nu med en Utbyta klass, konto kan fatta olika beslut när vi vill dra tillbaka Pengar i en valuta, men vi håller inte tillräckligt i den specifika valutan. Här är ett test som bättre exemplifierar det.

funktionstestItConvertsMoneyFromTheOtherCurrencyWhenWeDoNotHaveEnoughInTheCurrentOne () $ this-> account-> setPrimaryCurrency (Valuta :: USD ()); $ money = nya pengar (100, valuta :: USD ()); // Det är 1 USD $ this-> account-> deposition ($ money); $ This-> konto> setSecondaryCurrency (Valuta :: EUR ()); $ money = nya pengar (100, valuta :: EUR ()); // Det är 1 EURO = 1,5 USD $ this-> account-> deposition ($ money); $ this-> account-> withdraw (nya pengar (200, valuta :: USD ())); // Det är 2 USD $ this-> assertEquals (nya pengar (0, valuta :: USD ()), $ this-> account-> getPrimaryBalance ()); $ this-> assertEquals (nya pengar (34, valuta :: EUR ()), $ this-> account-> getSecondaryBalance ()); 

Vi ställer in vårt primära valuta till USD och deponerar en dollar. Då ställer vi den sekundära valutan till EUR och deponerar en euro. Då tar vi tillbaka två dollar. Slutligen förväntar vi oss att förbli med noll dollar och 0,34 euro. Självklart kastar detta test ett undantag, så vi måste genomföra en lösning på detta dilemma.

funktionen dra tillbaka (pengar $ pengar) $ this-> validateCurrencyFor ($ money); om $ this-> primaryCurrency == $ money-> getCurrency ()) om ($ this-> primaryBalance> = $ pengar) $ this-> primaryBalance = $ this-> primaryBalance-> subtraherar ($ money);  annars $ ourMoney = $ this-> primaryBalance-> add ($ this-> secondaryToPrimary ()); $ remainingMoney = $ ourMoney-> subtrahera ($ pengar); $ this-> primaryBalance = nya pengar (0, $ this-> primaryCurrency); $ this-> secondaryBalance = (new Exchange ()) -> konvertera ($ remainingMoney, $ this-> secondaryCurrency);  annat $ this-> secondaryBalance = $ this-> secondaryBalance-> subtrahera ($ pengar);  privat funktion secondaryToPrimary () return (new Exchange ()) -> konvertera ($ this-> secondaryBalance, $ this-> primaryCurrency); 

Wow, många förändringar måste göras för att stödja denna automatiska konvertering. Vad som händer är att om vi är i fråga om extrahering från vår primära valuta och vi inte har tillräckligt med pengar konverterar vi vår balans av sekundärvalutan till primär och försöker subtrahera igen. Om vi ​​fortfarande inte har tillräckligt med pengar, så $ ourMoney objektet kommer att slänga det lämpliga undantaget. Annars ställer vi in ​​vårt primära saldo till noll och vi kommer att konvertera de återstående pengarna tillbaka till den andra valutan och ställa in vår sekundära balans till det värdet.

Det är fortfarande upp till vårt konto logik att implementera en liknande automatisk konvertering för sekundär valuta. Vi kommer inte att genomföra en sådan symmetrisk logik. Om du gillar idén, anser du det som en övning för dig. Tänk också på en mer generisk privat metod som skulle göra magiken med automatisk omvandling i båda fallen.

Denna komplexa förändring i vår logik tvingar oss också att uppdatera en annan av våra test. När vi vill konvertera automatiskt måste vi ha en balans, även om det bara är noll.

/ ** * @expectedException Exception * @expectedExceptionMessage Subtraherade pengar är mer än vad vi har * / funktion testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nya pengar (100, valuta :: EUR ()); // Det är 1 EURO $ this-> account-> deposition ($ money); $ This-> konto> setSecondaryCurrency (valuta :: USD ()); $ money = nya pengar (0, valuta :: USD ()); $ This-> konto> insättning ($ pengar); $ this-> account-> withdraw (nya pengar (150, valuta :: EUR ())); 

Tilldela pengar mellan konton

Den sista metoden vi behöver genomföra på Pengar är allokera. Det här är logiken som bestämmer vad man ska göra när man delar pengar mellan olika konton som inte kan göras exakt. Om vi ​​till exempel har 0,10 cent och vi vill fördela dem mellan två konton i en andel av 30-70 procent, är det enkelt. Ett konto kommer att få tre cent och de övriga sju. Men om vi vill göra samma 30-70 förhållande fördelning på fem cent, har vi ett problem. Den exakta fördelningen skulle vara 1,5 cent i ett konto och 3,5 i den andra. Men vi kan inte dela cent, så vi måste implementera vår egen algoritm för att fördela pengarna.

Det kan finnas flera lösningar på det här problemet, en gemensam algoritm är att lägga till en cent i följd på varje konto. Om ett konto har mer cent än det exakta matematiska värdet, ska det elimineras från tilldelningslistan och inte ta emot några ytterligare pengar. Här är en grafisk representation.


Och ett test för att bevisa vår punkt är nedan.

funktion testItCanAllocateMoneyBetween2Accounts () $ a1 = $ this-> anAccount (); $ a2 = $ this-> anAccount (); $ money = nya pengar (5, valuta :: USD ()); $ money-> allokera ($ a1, $ a2, 30, 70); $ this-> assertEquals (nya pengar (2, valuta :: USD ()), $ a1-> getPrimaryBalance ()); $ this-> assertEquals (nya pengar (3, valuta :: USD ()), $ a2-> getPrimaryBalance ());  privat funktion anAccount () $ account = nytt konto (1); $ Konto> setPrimaryCurrency (valuta :: USD ()); $ konto-> insättning (nya pengar (0, valuta :: USD ())); returnera $ konto; 

Vi skapar bara en Pengar objekt med fem cent och två konton. Vi ringer allokera och förvänta sig att de två till tre värdena ska vara i de två kontona. Vi skapade också en hjälpmetod för att snabbt skapa konton. Testet misslyckas, som förväntat, men vi kan få det att passera ganska enkelt.

Funktionsallokering (Konto $ a1, Konto $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> belopp * $ a1Percent / 100; $ exactA2Balance = $ this-> amount * $ a2Percent / 100; $ oneCent = nya pengar (1, $ this-> currency); medan ($ this-> amount> 0) if ($ a1-> getPrimaryBalance () -> getAmount () < $exactA1Balance)  $a1->deposition ($ oneCent); $ This-> amount--;  om ($ this-> amount <= 0) break; if ($a2->getPrimaryBalance () -> getAmount () < $exactA2Balance)  $a2->deposition ($ oneCent); $ This-> amount--; 

Tja, inte den enklaste koden, men det fungerar korrekt, eftersom provningen av vårt test visar det. Det enda som vi fortfarande kan göra med den här koden är att minska den lilla dubbelarbete inuti medan slinga.

Funktionsallokering (Konto $ a1, Konto $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> belopp * $ a1Percent / 100; $ exactA2Balance = $ this-> amount * $ a2Percent / 100; medan ($ this-> amount> 0) $ this-> allocateTo ($ a1, $ exactA1Balance); om ($ this-> summa <= 0) break; $this->allocateTo ($ a2, $ exactA2Balance);  privat funktion allokeraTo ($ konto, $ exactBalance) if ($ account-> getPrimaryBalance () -> getAmount () < $exactBalance)  $account->insättning (nya pengar (1, $ this-> currency)); $ This-> amount--; 

Slutgiltiga tankar

Vad jag tycker är fantastiskt med det här lilla mönstret är det stora utbudet av fall där vi kan tillämpa det.

Vi är färdiga med vårt pengemönster. Vi såg att det är ett ganska enkelt mönster som inkapslar specifikationerna för pengakonceptet. Vi såg också att denna inkapsling lindrar belastningen av beräkningar från konto. Konto kan koncentrera sig på att representera konceptet från en högre nivå, ur bankens synvinkel. Konto kan implementera metoder som koppling till kontoinnehavare, ID, transaktioner och pengar. Det kommer att bli en orkestrator inte en räknare. Pengar tar hand om beräkningar.

Vad jag tycker är fantastiskt med det här lilla mönstret är det stora utbudet av fall där vi kan tillämpa det. I grund och botten kan du använda det varje gång du har ett värdeparpar. Tänk dig att du har en väderapplikation och du vill genomföra en temperaturpresentation. Det skulle motsvara vårt pengarobjekt. Du kan använda Fahrenheit eller Celsius som valutor.

Ett annat användarfall är när du har en kartläggningsansökan och du vill representera avstånd mellan poäng. Du kan enkelt använda det här mönstret för att växla mellan metriska eller kejserliga mätningar. När du arbetar med enkla enheter kan du släppa Exchange-objektet och implementera den enkla konverteringslogiken i ditt "Money" -objekt.

Så jag hoppas att du haft denna handledning och jag är ivrig att höra om de olika sätten du kan använda detta koncept. Tack för att du läste.