Pengemønster Den riktige måten å representere Value-Unit Par

Pengemønsteret, definert av Martin Fowler og publisert i Mønster av Enterprise Application Architecture, er en fin måte å representere verdi-enhetspar på. Det kalles Money Pattern fordi det oppstod i en finansiell sammenheng, og vi vil illustrere bruken av dette hovedsakelig i denne sammenheng ved hjelp av PHP.


En PayPal Like-konto

Jeg aner ikke hvordan PayPal er implementert, men jeg synes det er en god ide å ta funksjonen som et eksempel. La meg vise deg hva jeg mener, min PayPal-konto har to valutaer: amerikanske dollar og euro. Det holder de to verdiene skilt, men jeg kan motta penger i hvilken som helst valuta, jeg kan se mitt totale beløp i noen av de to valutaene, og jeg kan trekke ut i noen av de to. For dette eksemplets skyld, tenk at vi trekker ut i noen av valutaene, og automatisk konvertering er gjort hvis balansen i den aktuelle valutaen er mindre enn hva vi ønsker å overføre, men likevel er det fortsatt nok penger i den andre valutaen. Også, vi vil begrense eksemplet til bare to valutaer.


Skaffe en konto

Hvis jeg skulle opprette og bruke et Kontoobjekt, vil jeg initialisere det med et kontonummer.

funksjon testItCanCrateANewAccount () $ this-> assertInstanceOf ("Konto", ny konto (123)); 

Dette vil tydeligvis mislykkes fordi vi ikke har noen konto klassen ennå.

klassekonto 

Vel, skriv det i en ny "Account.php" fil og krever det i testen, fikk det til å passere. Dette gjøres imidlertid bare for å gjøre oss selv komfortable med ideen. Deretter tenker jeg på å få kontoen id.

funksjon testItCanCrateANewAccountWithId () $ this-> assertEquals (123, (ny konto (123)) -> getId ()); 

Jeg har faktisk endret den forrige testen til denne. Det er ingen grunn til å beholde den første. Det levde det livet, noe som betyr at det tvang meg til å tenke på Konto klasse og faktisk lage den. Vi kan nå fortsette.

klassekonto privat $ id; funksjon __construct ($ id) $ this-> id = $ id;  offentlig funksjon getId () return $ this-> id; 

Testen går forbi og Konto begynner å se ut som en ekte klasse.


valutaer

Basert på vår PayPal-analogi, vil vi kanskje definere en primær og en sekundær valuta for vår konto.

privat $ konto; beskyttet funksjon setUp () $ this-> account = ny konto (123);  [...] funksjonstestItCanHavePrimaryAndSecondaryCurrencies () $ this-> account-> setPrimaryCurrency ("EUR"); $ Dette-> konto-> setSecondaryCurrency ( 'USD'); $ this-> assertEquals (array ('primary' => 'EUR', 'sekundær' => 'USD'), $ this-> account-> getCurrencies ()); 

Nå vil testen ovenfor tvinge oss til å skrive følgende kode.

klassekonto privat $ id; privat $ primaryCurrency; privat $ secondaryCurrency; [...] funksjon setPrimaryCurrency ($ valuta) $ this-> primaryCurrency = $ currency;  funksjon setSekundærValuta ($ valuta) $ this-> secondaryCurrency = $ currency;  funksjon getCurrencies () return array ('primary' => $ this-> primaryCurrency, 'secondary' => $ this-> secondaryCurrency); 

For tiden holder vi valuta som en enkel streng. Dette kan endres i fremtiden, men vi er ikke der ennå.


Gimme pengene

Det er endeløse grunner til ikke å representere penger som en enkel verdi. Flytepunktsberegninger? Hvem som helst? Hva med valuta fraksjoner? Skal vi ha 10, 100 eller 1000 cent i noen eksotisk valuta? Vel, dette er et annet problem vi må unngå. Hva med å dele uendelige sent?

Det er bare for mange og eksotiske problemer når du jobber med penger for å skrive dem ned i koden, så vi vil gå direkte til løsningen, Money Pattern. Dette er ganske enkelt et mønster, med store fordeler og mange brukssaker, langt utenfor finansdomenet. Når du må representere et verdi-enhetspar, bør du sannsynligvis bruke dette mønsteret.


Pengemønsteret er i utgangspunktet en klasse som innkapsler et beløp og en valuta. Deretter definerer den alle matematiske operasjoner på verdien i forhold til valutaen. "tildele()" er en spesiell funksjon for å fordele en bestemt sum penger mellom to eller flere mottakere.

Så, som bruker av Penger Jeg vil gjerne kunne gjøre dette i en test:

klasse MoneyTest utvider PHPUnit_Framework_TestCase funksjonstestWeCanCreateAMoneyObject () $ money = nye penger (100, valuta :: USD ()); 

Men det vil ikke fungere enda. Vi trenger begge deler Penger og Valuta. Enda mer trenger vi Valuta før Penger. Dette blir en enkel klasse, så jeg vil hoppe over å teste det for nå. Jeg er ganske sikker på at IDE kan generere mesteparten av koden for meg.

klasse Valuta privat $ centFactor; privat $ stringRepresentation; privat funksjon __construct ($ centFactor, $ stringRepresentation) $ this-> centFactor = $ centFactor; $ this-> stringRepresentation = $ stringRepresentation;  offentlig funksjon getCentFactor () return $ this-> centFactor;  funksjon getStringRepresentation () return $ this-> stringRepresentation;  statisk funksjon USD () returner nytt selv (100, 'USD');  statisk funksjon EUR () returnere nytt selv (100, 'EUR'); 

Det er nok for vårt eksempel. Vi har to statiske funksjoner for USD og EUR valutaer. I en ekte applikasjon vil vi sannsynligvis ha en generell konstruktør med en parameter og laste alle valutaene fra en database tabell eller enda bedre fra en tekstfil.

Deretter inkluderer du de to nye filene i testen:

require_once '... /Currency.php'; require_once '... /Money.php'; klasse MoneyTest utvider PHPUnit_Framework_TestCase funksjonstestWeCanCreateAMoneyObject () $ money = nye penger (100, valuta :: USD ()); 

Denne testen feiler fortsatt, men i det minste kan den finne Valuta nå. Vi fortsetter med en minimal Penger gjennomføring. Litt mer enn hva denne testen strengt krever siden det er igjen, for det meste automatisk generert kode.

klasse penger privat $ beløp; privat $ valuta; funksjon __construct ($ beløp, Valuta $ valuta) $ this-> amount = $ amount; $ this-> valuta = $ valuta; 

Vær oppmerksom på at vi håndhever typen Valuta for den andre parameteren i vår konstruktør. Dette er en fin måte å unngå at våre kunder sender i søppel som valuta.


Sammenligne penger

Det første som kom inn i tankene mine etter at jeg hadde det minste objektet oppe, var at jeg måtte sammenligne pengerobjekter på en eller annen måte. Da husket jeg at PHP er ganske smart når det gjelder å sammenligne objekter, så jeg skrev denne testen.

funksjon testItCanTellTwoMoneyObjectAreEqual () $ m1 = new Money (100, Valuta :: USD ()); $ m2 = nye penger (100, valuta :: USD ()); $ Dette-> assertEquals ($ m1, m2 $); $ this-> assertTrue ($ m1 == $ m2); 

Vel, det går faktisk. De "assertEquals" funksjonen kan sammenligne de to objektene og til og med den innebygde likestillingen fra PHP "==" forteller meg hva jeg forventer. Hyggelig.

Men hva om vi er interessert i at en er større enn den andre? Til min enda større overraskelse, går den følgende testen også uten problemer.

funksjon testOneMoneyIsBiggerThanTheOther () $ m1 = new Money (200, Valuta :: USD ()); $ m2 = nye penger (100, valuta :: USD ()); $ this-> assertGreaterThan ($ m2, $ m1); $ this-> assertTrue ($ m1> $ m2); 

Som fører oss til å ...

funksjon testOneMoneyIsLessThanTheOther () $ m1 = new Money (100, Valuta :: USD ()); $ m2 = nye penger (200, valuta :: USD ()); $ this-> assertLessThan ($ m2, $ m1); $ Dette-> assertTrue ($ m1 < $m2); 

... en test som passerer umiddelbart.


Plus, Minus, Multiply

Å se så mye PHP magi faktisk jobber med sammenligninger, jeg kunne ikke motstå å prøve denne.

funksjon testTwoMoneyObjectsCanBeAdded () $ m1 = nye penger (100, valuta :: USD ()); $ m2 = nye penger (200, valuta :: USD ()); $ sum = nye penger (300, valuta :: USD ()); $ this-> assertEquals ($ sum, $ m1 + $ m2); 

Som feiler og sier:

Objekt av klasse Pengene kunne ikke konverteres til int

Hmm. Det høres ganske tydelig ut. På dette punktet må vi ta en beslutning. Det er mulig å fortsette denne øvelsen med enda mer PHP magi, men denne tilnærmingen vil på et tidspunkt forvandle denne opplæringen til et PHP-cheatsheet i stedet for et designmønster. Så la oss ta beslutningen om å implementere de faktiske metodene for å legge til, trekke ut og formere pengerobjekter.

funksjon testTwoMoneyObjectsCanBeAdded () $ m1 = nye penger (100, valuta :: USD ()); $ m2 = nye penger (200, valuta :: USD ()); $ sum = nye penger (300, valuta :: USD ()); $ this-> assertEquals ($ sum, $ m1-> add ($ m2)); 

Denne testen feiler også, men med en feil som forteller oss er det nei "Legg til" metode på Penger.

offentlig funksjon getAmount () return $ this-> amount;  funksjon legg til ($ andre) returner nye penger ($ dette-> beløp + $ andre-> getAmount (), $ this-> valuta); 

For å oppsummere to Penger objekter, vi trenger en måte å hente størrelsen på objektet vi sender inn som argumentet. Jeg foretrekker å skrive en getter, men å sette klassevariabelen til å være offentlig ville også være en akseptabel løsning. Men hva om vi vil legge til dollar til euro?

/ ** * @expectedException Exception * @expectedExceptionMessage Begge penger må være av samme valuta * / funksjon testItThrowsExceptionIfWeTryToAddTwoMoneysWithDifferentCurrency () $ m1 = new Money (100, Valuta :: USD ()); $ m2 = nye penger (100, valuta :: EUR ()); $ M1> legg ($ m2); 

Det er flere måter å håndtere operasjoner på Penger objekter med forskjellige valutaer. Vi vil kaste et unntak og forvente det i testen. Alternativt kan vi implementere en valutaomregningsmekanisme i vår søknad, ring den, konvertere begge Penger objekter i noen standard valuta og sammenligne dem. Eller hvis vi ville ha en mer sofistikert valutaomregningsalgoritme, kunne vi alltid konvertere fra en til en annen og sammenligne i den konverterte valutaen. Saken er at når konvertering kommer på plass, må konverteringsavgifter vurderes og ting blir ganske komplisert. Så la oss bare kaste det unntaket og fortsette.

offentlig funksjon getCurrency () return $ this-> currency;  funksjon legge til (Money $ andre) $ this-> sikreSameCurrencyWith ($ andre); returnere nye penger ($ this-> amount + $ other-> getAmount (), $ this-> valuta);  Privat funksjon sikreSameCurrencyWith (Money $ andre) if ($ this-> valuta! = $ other-> getCurrency ()) kaste ny unntak ("begge pengene må være av samme valuta"); 

Det er bedre. Vi gjør en sjekk for å se om valutaene er forskjellige og kaste et unntak. Jeg skrev det allerede som en egen privat metode, fordi jeg vet at vi vil trenge det i de andre matematiske operasjonene også.

Subtraksjon og multiplikasjon ligner veldig på tillegg, så her er koden og du kan finne testene i vedlagte kildekoden.

funksjon subtrahere (Money $ andre) $ this-> sikreSameCurrencyWith ($ andre); hvis ($ andre> $ dette) kaste ny unntak ("Subtraherte penger er mer enn det vi har"); returnere nye penger ($ this-> amount - $ other-> getAmount (), $ this-> valuta);  funksjon multiplyBy ($ multiplikator, $ roundMethod = PHP_ROUND_HALF_UP) $ product = runde ($ this-> amount * $ multiplikator, 0, $ roundMethod); returnere nye penger ($ produkt, $ this-> valuta); 

Med subtraksjon må vi sørge for at vi har nok penger og med multiplikasjon, må vi gjøre handlinger for å runde ting opp eller ned, slik at deling (multiplikasjon med tall mindre enn en) ikke vil produsere "halvcents". Vi beholder beløpet i cent, den lavest mulige faktoren for valutaen. Vi kan ikke dele det mer.


Vi presenterer valuta for vår konto

Vi har en nesten fullført Penger og Valuta. Det er på tide å introdusere disse objektene til Konto. Vi begynner med Valuta, og endre våre tester tilsvarende.

funksjonstestItCanHavePrimaryAndSecondaryCurrencies () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ Dette-> konto-> setSecondaryCurrency (valuta :: USD ()); $ this-> assertEquals (array ('primary' => Valuta :: EUR (), 'sekundær' => Valuta :: USD ()), $ this-> konto-> getCurrencies ()); 

På grunn av PHP's dynamiske skrivefaktor, går denne testen uten problemer. Men jeg vil gjerne tvinge metodene i Konto å bruke Valuta objekter og ikke godta noe annet. Dette er ikke obligatorisk, men jeg finner slike typer hintings ekstremt nyttige når noen andre trenger å forstå vår kode.

funksjon setPrimaryCurrency (Valuta $ valuta) $ this-> primaryCurrency = $ currency;  funksjon setSekundærValuta (Valuta $ valuta) $ this-> secondaryCurrency = $ valuta; 

Nå er det åpenbart for alle som leser denne koden for første gang det Konto fungerer med Valuta.


Vi presenterer penger på vår konto

De to grunnleggende handlingene som en konto må gi, er: innskudd - som betyr å legge til penger på en konto - og trekke tilbake - noe som betyr å fjerne penger fra en konto. Innskudd har en kilde og uttak har en annen destinasjon enn vår nåværende konto. Vi vil ikke gå inn på detaljer om hvordan disse transaksjonene skal gjennomføres, vi vil bare konsentrere oss om å implementere effektene de har på vår konto. Så kan vi forestille oss en test som dette for deponering.

funksjon testAccountCanDepositMoney () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nye penger (100, valuta :: EUR ()); // Det er 1 EURO $ dette-> konto-> innskudd ($ penger); $ this-> assertEquals ($ money, $ this-> account-> getPrimaryBalance ()); 

Dette vil tvinge oss til å skrive ganske mye implementeringskode.

klassekonto privat $ id; privat $ primaryCurrency; privat $ secondaryCurrency; privat $ secondaryBalance; privat $ primaryBalance; funksjon getSecondaryBalance () return $ this-> secondaryBalance;  funksjon getPrimaryBalance () return $ this-> primaryBalance;  funksjon __construct ($ id) $ this-> id = $ id;  [...] funksjon innskudd (Money $ money) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ money: $ this-> secondaryBalance = $ money; 

OK, OK. Jeg vet, jeg skrev mer enn det som var absolutt nødvendig, for produksjon. Men jeg vil ikke bore deg i døden med baby-trinn, og jeg er også ganske sikker på at koden for secondaryBalance vil fungere riktig. Det var nesten helt generert av IDE. Jeg vil til og med hoppe over å teste den. Mens denne koden gjør testen vår, må vi spørre oss hva som skjer når vi gjør etterfølgende innskudd? Vi vil at våre penger skal legges til forrige saldo.

funksjonstestSubsequentDepositsAddUpTheMoney () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nye penger (100, valuta :: EUR ()); // Det er 1 EURO $ dette-> konto-> innskudd ($ penger); // En euro i kontoen $ this-> account-> innskudd ($ penger); // To euro i kontoen $ this-> assertEquals ($ money-> multiplyBy (2), $ this-> account-> getPrimaryBalance ()); 

Vel, det mislykkes. Så vi må oppdatere vår produksjonskode.

funksjonsinnbetaling (Money $ money) if ($ this-> primaryCurrency == $ money-> getCurrency ()) $ this-> primaryBalance = $ this-> primaryBalance? : Nye penger (0, $ this-> primaryCurrency); $ this-> primaryBalance = $ this-> primaryBalance-> add ($ money);  ellers $ this-> secondaryBalance = $ this-> secondaryBalance? : Nye penger (0, $ this-> secondaryCurrency); $ this-> secondaryBalance = $ this-> secondaryBalance-> add ($ money); 

Dette er mye bedre. Vi er sannsynligvis ferdig med innskudd metode og vi kan fortsette med ta ut.

funksjon testAccountCanWithdrawMoneyOfSameCurrency () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nye penger (100, valuta :: EUR ()); // Det er 1 EURO $ dette-> konto-> innskudd ($ penger); $ this-> account-> withdraw (nye penger (70, valuta :: EUR ())); $ this-> assertEquals (new Money (30, Valuta :: EUR ()), $ this-> account-> getPrimaryBalance ()); 

Dette er bare en enkel test. Løsningen er enkel også.

funksjon trekke tilbake (Money $ money) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> trekke ned ($ penger): $ this-> secondaryBalance = $ this-> secondaryBalance-> trekke ned ($ money); 

Vel, det fungerer, men hva hvis vi vil bruke en Valuta det er ikke på vår konto? Vi burde kaste en eksklusjon for det.

/ ** * @expectedException Exception * @expectedExceptionMessage Denne kontoen har ingen valuta USD * / funksjonstestThrowsExceptionForInexistentCurrencyOnWithdraw () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nye penger (100, valuta :: EUR ()); // Det er 1 EURO $ dette-> konto-> innskudd ($ penger); $ this-> account-> withdraw (nye penger (70, valuta :: USD ())); 

Det vil også tvinge oss til å sjekke våre valutaer.

funksjon trekke tilbake (penger $ penger) $ this-> validateCurrencyFor ($ money); $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> trekke ned ($ penger): $ this-> secondaryBalance = $ this-> secondaryBalance-> trekke ned ($ money);  privat funksjon validateCurrencyFor (Money $ money) hvis (! in_array ($ money-> getCurrency (), $ this-> getCurrencies ())) kaste ny Unntak (sprintf ('Denne kontoen har ingen valuta% s', $ penger -> getCurrency () -> getStringRepresentation ())); 

Men hva om vi ønsker å trekke tilbake mer enn hva vi har? Det saken var allerede tatt opp da vi implementerte subtraksjon på Penger. Her er testen som viser det.

/ ** * @expectedException Exception * @expectedExceptionMessage Subtraherte penger er mer enn hva vi har * / funksjon testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nye penger (100, valuta :: EUR ()); // Det er 1 EURO $ dette-> konto-> innskudd ($ penger); $ this-> account-> withdraw (nye penger (150, valuta :: EUR ())); 

Å håndtere uttak og utveksling

En av de vanskeligste tingene å håndtere når vi jobber med flere valutaer, bytter mellom dem. Skjønnheten i dette mønsteret er at det gjør at vi kan forenkle dette problemet ved å isolere og innkapslere det i sin egen klasse. Mens logikken i en Utveksling klassen kan være svært sofistikert, bruk blir mye lettere. For denne tutorials skyld, la oss forestille oss at vi har noen veldig grunnleggende Utveksling bare logikk. 1 EUR = 1,5 USD.

Klasse Exchange Funksjon konvertere (Money $ Money, Currency $ to Currency) if ($ toCurrency == Valuta :: EUR () && $ money-> getCurrency () == Valuta :: USD ()) returnere nye penger -> multiplyBy (0.67) -> getAmount (), $ toCurrency); hvis ($ toCurrency == Valuta :: USD () && $ money-> getCurrency () == Valuta :: EUR ()) returnere nye penger ($ money-> multiplyBy (1.5) -> getAmount (), $ til Valuta) ; returner $ penger; 

Hvis vi konverterer fra EUR til USD, multipliserer vi verdien med 1,5, hvis vi konverterer fra USD til EUR, deler vi verdien med 1,5, ellers antar vi at vi konverterer to valutaer av samme type, slik at vi ikke gjør noe og bare returnerer pengene . Selvfølgelig vil dette i virkeligheten være en mye mer komplisert klasse.

Nå, ha en Utveksling klasse, Konto kan ta forskjellige beslutninger når vi ønsker å trekke tilbake Penger i en valuta, men vi holder ikke nok i den aktuelle valutaen. Her er en test som bedre illustrerer den.

funksjon testItConvertsMoneyFromTheOtherCurrencyWhenWeDoNotHaveEnoughInTheCurrentOne () $ this-> account-> setPrimaryCurrency (Valuta :: USD ()); $ money = nye penger (100, valuta :: USD ()); // Det er 1 USD $ this-> konto-> innskudd ($ penger); $ Dette-> konto-> setSecondaryCurrency (Currency :: EUR ()); $ money = nye penger (100, valuta :: EUR ()); // Det er 1 EURO = 1,5 USD $ this-> konto-> innskudd ($ penger); $ this-> account-> withdraw (nye penger (200, valuta :: USD ())); // Det er 2 USD $ this-> assertEquals (nye penger (0, valuta :: USD ()), $ this-> account-> getPrimaryBalance ()); $ this-> assertEquals (nye penger (34, valuta :: EUR ()), $ this-> account-> getSecondaryBalance ()); 

Vi setter vår konto primærvaluta til USD og deponerer en dollar. Deretter setter vi den sekundære valutaen til EUR og deponerer en Euro. Så trekker vi ut to dollar. Til slutt forventer vi å forbli med null dollar og 0,34 euro. Selvfølgelig kaster denne testen et unntak, så vi må implementere en løsning på dette dilemmaet.

funksjon trekke tilbake (penger $ penger) $ this-> validateCurrencyFor ($ money); hvis ($ this-> primaryCurrency == $ money-> getCurrency ()) hvis ($ this-> primaryBalance> = $ penger) $ this-> primaryBalance = $ this-> primaryBalance-> trekker ned ($ penger);  ellers $ ourMoney = $ this-> primaryBalance-> add ($ this-> secondaryToPrimary ()); $ remainingMoney = $ ourMoney-> trekke ned ($ penger); $ this-> primaryBalance = nye penger (0, $ this-> primaryCurrency); $ this-> secondaryBalance = (new Exchange ()) -> konverter ($ resterendeLønn, $ denne-> sekundærKurrency);  ellers $ this-> secondaryBalance = $ this-> secondaryBalance-> trekker ned ($ penger);  privat funksjon secondaryToPrimary () return (new Exchange ()) -> konverter ($ this-> secondaryBalance, $ this-> primaryCurrency); 

Wow, mange endringer måtte gjøres for å støtte denne automatiske konvertering. Det som skjer er at hvis vi er ved å trekke ut fra vår primære valuta, og vi ikke har nok penger, konverterer vi balansen til den sekundære valutaen til primær og prøver subtraksjonen igjen. Hvis vi fortsatt ikke har nok penger, vil $ ourMoney objektet vil kaste det riktige unntaket. Ellers vil vi sette vår primære saldo til null, og vi vil konvertere de resterende pengene tilbake til den sekundære valutaen og sette vår sekundære saldo til den verdien.

Det er fortsatt opp til logikkens logikk for å implementere en lignende automatisk konvertering for sekundær valuta. Vi vil ikke implementere en slik symmetrisk logikk. Hvis du liker ideen, bør du vurdere det som en øvelse for deg. Også, tenk på en mer generisk privat metode som ville gjøre magien til automatisk konverteringer i begge tilfeller.

Denne komplekse endringen i logikken vår tvinger oss også til å oppdatere en annen av våre tester. Når vi ønsker å konvertere automatisk, må vi ha en balanse, selv om den bare er null.

/ ** * @expectedException Exception * @expectedExceptionMessage Subtraherte penger er mer enn hva vi har * / funksjon testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> setPrimaryCurrency (Valuta :: EUR ()); $ money = nye penger (100, valuta :: EUR ()); // Det er 1 EURO $ dette-> konto-> innskudd ($ penger); $ Dette-> konto-> setSecondaryCurrency (valuta :: USD ()); $ money = new Money (0, Valuta :: USD ()); $ Dette-> konto-> innskudd ($ penger); $ this-> account-> withdraw (nye penger (150, valuta :: EUR ())); 

Fordeling av penger mellom kontoer

Den siste metoden vi trenger å implementere på Penger er tildele. Dette er logikken som bestemmer hva du skal gjøre når du deler penger mellom ulike kontoer som ikke kan gjøres nøyaktig. For eksempel, hvis vi har 0,10 cent, og vi vil tildele dem mellom to kontoer i en andel på 30-70 prosent, er det enkelt. En konto vil få tre cent og de andre syv. Men hvis vi vil gjøre samme 30-70 forholdsallokering på fem cent, har vi et problem. Den nøyaktige tildelingen ville være 1,5 cent i en konto og 3,5 i den andre. Men vi kan ikke dele sent, så vi må implementere vår egen algoritme for å fordele pengene.

Det kan være flere løsninger på dette problemet. En vanlig algoritme er å legge til en sekvens i rekkefølge til hver konto. Hvis en konto har flere cent enn den eksakte matematiske verdien, bør den elimineres fra allokeringslisten og ikke motta ytterligere penger. Her er en grafisk representasjon.


Og en test for å bevise vårt poeng er nedenfor.

funksjon testItCanAllocateMoneyBetween2Accounts () $ a1 = $ this-> anAccount (); $ a2 = $ this-> anAccount (); $ money = nye penger (5, valuta :: USD ()); $ money-> alloker ($ a1, $ a2, 30, 70); $ this-> assertEquals (nye penger (2, valuta :: USD ()), $ a1-> getPrimaryBalance ()); $ this-> assertEquals (nye penger (3, valuta :: USD ()), $ a2-> getPrimaryBalance ());  privat funksjon anAccount () $ account = ny konto (1); $ Konto-> setPrimaryCurrency (valuta :: USD ()); $ konto-> innskudd (nye penger (0, valuta :: USD ())); returnere $ konto; 

Vi lager bare en Penger objekt med fem cent og to kontoer. Vi ringer tildele og forvent at de to til tre verdiene skal være i de to kontoene. Vi har også opprettet en hjelpemetode for å raskt opprette kontoer. Testen feiler, som forventet, men vi kan få det til å passere ganske enkelt.

funksjonallokering (Konto $ a1, Konto $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> beløp * $ a1Percent / 100; $ exactA2Balance = $ this-> amount * $ a2Percent / 100; $ oneCent = nye penger (1, $ this-> valuta); mens ($ dette-> beløp> 0) hvis ($ a1-> getPrimaryBalance () -> getAmount () < $exactA1Balance)  $a1->innskudd ($ oneCent); $ Dette-> amount--;  hvis ($ dette-> beløp <= 0) break; if ($a2->getPrimaryBalance () -> getAmount () < $exactA2Balance)  $a2->innskudd ($ oneCent); $ Dette-> amount--; 

Vel, ikke den enkleste koden, men den fungerer som den skal, da passering av testen viser det. Det eneste vi fortsatt kan gjøre med denne koden er å redusere den lille duplikasjonen inne i samtidig som sløyfe.

funksjonallokering (Konto $ a1, Konto $ a2, $ a1Percent, $ a2Percent) $ exactA1Balance = $ this-> beløp * $ a1Percent / 100; $ exactA2Balance = $ this-> amount * $ a2Percent / 100; mens ($ this-> amount> 0) $ this-> allocateTo ($ a1, $ exactA1Balance); hvis ($ dette-> beløp <= 0) break; $this->allocateTo ($ a2, $ exactA2Balance);  privat funksjon allocateTo ($ konto, $ exactBalance) if ($ account-> getPrimaryBalance () -> getAmount () < $exactBalance)  $account->innskudd (nye penger (1, $ denne-> valuta)); $ Dette-> amount--; 

Siste tanker

Det jeg finner utrolig med dette lille mønsteret, er det store spekteret av tilfeller der vi kan bruke det.

Vi er ferdige med våre pengemønster. Vi så at det er ganske enkelt et mønster, som inkapsler spesifikasjonene til pengekonseptet. Vi så også at denne innkapslingen lindrer byrden av beregninger fra Konto. Konto kan konsentrere seg om å representere konseptet fra et høyere nivå, fra bankens synspunkt. Konto kan implementere metoder som for eksempel forbindelse med kontoinnehavere, IDer, transaksjoner og penger. Det vil bli en orkestrator ikke en kalkulator. Penger vil ta seg av beregninger.

Det jeg finner utrolig med dette lille mønsteret, er det store spekteret av tilfeller der vi kan bruke det. I utgangspunktet, hver gang du har et verdi-enhetspar, kan du bruke det. Tenk deg at du har et værprogram og du vil implementere en representasjon for temperaturen. Det ville være lik vår pengepost. Du kan bruke Fahrenheit eller Celsius som valuta.

En annen brukstilfelle er når du har en kartleggingsapplikasjon, og du vil representere avstander mellom punkter. Du kan enkelt bruke dette mønsteret til å bytte mellom metriske eller keiserlige målinger. Når du arbeider med enkle enheter, kan du slippe Exchange-objektet og implementere den enkle konverteringslogikken i ditt "Money" -objekt.

Så, jeg håper du likte denne opplæringen, og jeg er ivrig etter å høre om de forskjellige måtene du kan bruke dette konseptet. Takk for at du leser.