Le modèle monétaire le bon moyen de représenter les paires valeur-unités

Le modèle Money, défini par Martin Fowler et publié dans Patterns of Enterprise Application Architecture, est un excellent moyen de représenter des paires valeur-unité. Il s’appelle Money Pattern car il est apparu dans un contexte financier et nous illustrerons son utilisation principalement dans ce contexte avec PHP..


Un compte PayPal

Je ne sais pas du tout comment PayPal est implémenté, mais je pense que c'est une bonne idée de prendre ses fonctionnalités comme exemple. Laissez-moi vous montrer ce que je veux dire, mon compte PayPal a deux devises: le dollar américain et l’euro. Il conserve les deux valeurs séparées, mais je peux recevoir de l’argent dans n’importe quelle devise, je peux voir le montant total dans n’importe laquelle des deux devises et extraire dans n’importe laquelle des deux. Pour illustrer cet exemple, imaginons que nous extrayions dans n'importe quelle devise et que la conversion automatique soit effectuée si le solde de cette devise spécifique est inférieur à celui que nous souhaitons virer tout en conservant suffisamment d'argent dans l'autre devise. En outre, nous limiterons l’exemple à deux devises seulement..


Obtenir un compte

Si je devais créer et utiliser un objet Compte, j'aimerais l'initialiser avec un numéro de compte..

function testItCanCrateANewAccount () $ this-> assertInstanceOf ("Compte", nouveau compte (123)); 

Cela va évidemment échouer parce que nous n'avons pas encore de classe Account.

compte de classe 

Eh bien, en écrivant cela dans un nouveau "Account.php" fichier et le nécessitant dans le test, le fait passer. Cependant, tout cela est fait juste pour nous mettre à l'aise avec l'idée. Ensuite, je pense à obtenir le compte identifiant.

fonction testItCanCrateANewAccountWithId () $ this-> assertEquals (123, (nouveau compte (123)) -> getId ()); 

J'ai en fait changé le test précédent en celui-ci. Il n'y a aucune raison de garder le premier. Il a vécu sa vie, ce qui signifie que cela m'a obligé à penser à la Compte classe et effectivement le créer. Nous pouvons maintenant passer à autre chose.

compte classe private $ id; fonction __construct ($ id) $ this-> id = $ id;  fonction publique getId () return $ this-> id; 

Le test passe et Compte commence à ressembler à une vraie classe.


Monnaies

Sur la base de notre analogie avec PayPal, nous pouvons définir une devise principale et une devise secondaire pour notre compte..

compte privé $; fonction protégée setUp () $ this-> compte = nouveau compte (123);  […] Function testItCanHavePrimaryAndSecondaryCurrency () $ this-> account-> setPrimaryCurrency ("EUR"); $ this-> account-> setSecondaryCurrency ('USD'); $ this-> assertEquals (array ('primary' => 'EUR', 'secondary' => 'USD'), $ this-> compte-> getC devises ()); 

Maintenant, le test ci-dessus nous obligera à écrire le code suivant.

compte classe private $ id; private $ primaryCurrency; private $ secondaryCurrency; […] Function setPrimaryCurrency ($ currency) $ this-> primaryCurrency = $ currency;  function setSecondaryCurrency ($ currency) $ this-> secondaryCurrency = $ currency;  function getCurrency () return array ('primary' => $ this-> primaryCurrency, 'secondary' => $ this-> secondaryCurrency); 

Pour le moment, nous conservons la devise comme une simple chaîne. Cela peut changer dans le futur, mais nous n'y sommes pas encore.


Donne-moi l'argent

Il y a d'innombrables raisons pour lesquelles ne pas représenter l'argent comme une simple valeur. Calculs à virgule flottante? N'importe qui? Qu'en est-il des fractions de monnaie? Devrions-nous avoir 10, 100 ou 1000 cents dans une monnaie exotique? Eh bien, c’est un autre problème que nous devrons éviter. Qu'en est-il de l'allocation de centimes indivisibles??

Lorsque vous travaillez avec de l'argent, vous rencontrez trop de problèmes exotiques et que vous les écrivez dans le code. Nous allons donc passer directement à la solution, le modèle d'argent. C'est un modèle assez simple, avec de grands avantages et de nombreux cas d'utilisation, loin du domaine financier. Chaque fois que vous devez représenter une paire valeur-unité, vous devriez probablement utiliser ce modèle.


Le modèle Money est fondamentalement une classe encapsulant un montant et une devise. Ensuite, il définit toutes les opérations mathématiques sur la valeur par rapport à la devise. "allouer()" est une fonction spéciale permettant de répartir un montant spécifique entre deux destinataires ou plus.

Donc, en tant qu'utilisateur de Argent J'aimerais pouvoir faire cela lors d'un test:

la classe MoneyTest étend PHPUnit_Framework_TestCase function testWeCanCreateAMoneyObject () $ money = new Money (100, Currency :: USD ()); 

Mais ça ne marchera pas encore. Nous avons besoin des deux Argent et Devise. Encore plus, nous avons besoin Devise avant Argent. Ce sera un cours simple, donc je ne le testerai pas pour le moment. Je suis à peu près sûr que l'EDI peut générer la plupart du code pour moi.

classe Currency private $ centFactor; private $ stringRepresentation; fonction privée __construct ($ centFactor, $ stringRepresentation) $ this-> centFactor = $ centFactor; $ this-> stringRepresentation = $ stringRepresentation;  fonction publique getCentFactor () return $ this-> centFactor;  function getStringRepresentation () return $ this-> stringRepresentation;  fonction statique USD () retour nouveau self (100, 'USD');  fonction statique EUR () return new self (100, 'EUR'); 

Cela suffit pour notre exemple. Nous avons deux fonctions statiques pour les devises USD et EUR. Dans une application réelle, nous aurions probablement un constructeur général avec un paramètre et chargerions toutes les devises depuis une table de base de données ou, mieux encore, depuis un fichier texte..

Ensuite, incluez les deux nouveaux fichiers dans le test:

require_once '… /Currency.php'; require_once '… /Money.php'; la classe MoneyTest étend PHPUnit_Framework_TestCase function testWeCanCreateAMoneyObject () $ money = new Money (100, Currency :: USD ()); 

Ce test échoue toujours, mais au moins il peut trouver Devise à présent. Nous continuons avec un minimum Argent la mise en oeuvre. Un peu plus que ce que ce test nécessite strictement puisqu'il s'agit, là encore, de code principalement généré automatiquement.

classe Money montant privé $; devise privée; fonction __construct (montant $, devise $ devise) $ ce-> montant = $ montant; $ this-> devise = $ devise; 

S'il vous plaît noter, nous appliquons le type Devise pour le second paramètre dans notre constructeur. C’est un bon moyen d’éviter que nos clients envoient du courrier indésirable comme devise..


Comparer de l'argent

La première chose qui m'est venue à l'esprit après la mise en place de cet objet minimal était que je devrais comparer les objets d'argent d'une manière ou d'une autre. Ensuite, je me suis rappelé que PHP est assez intelligent pour comparer des objets, alors j’ai écrit ce test..

fonction testItCanTellTwoMoneyObjectAreEqual () $ m1 = new Money (100, Currency :: USD ()); $ m2 = nouvel argent (100, devise :: USD ()); $ this-> assertEquals ($ m1, $ m2); $ this-> assertTrue ($ m1 == $ m2); 

Eh bien, cela passe réellement. le "assertEquals" fonction peut comparer les deux objets et même la condition d'égalité intégrée de PHP "==" me dit ce que j'attends. Agréable.

Mais qu'en est-il si nous souhaitons que l’un soit plus grand que l’autre? A ma plus grande surprise encore, le test suivant passe également sans problème.

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

Ce qui nous amène à…

function testOneMoneyIsLessThanTheOther () $ m1 = new Money (100, Devise :: USD ()); $ m2 = nouvel argent (200, monnaie :: USD ()); $ this-> assertLessThan ($ m2, $ m1); $ this-> assertTrue ($ m1 < $m2); 

… Un test qui passe immédiatement.


Plus, moins, multiplier

Voyant que la magie de PHP fonctionnait réellement avec des comparaisons, je n’ai pas pu résister à l’essayer..

fonction testTwoMoneyObjectsCanBeAdded () $ m1 = new Money (100, Devise :: USD ()); $ m2 = nouvel argent (200, monnaie :: USD ()); $ sum = new Money (300, Currency :: USD ()); $ this-> assertEquals (somme $, m1 $ + m2); 

Ce qui échoue et dit:

L'objet de la classe Money n'a pas pu être converti en int

Hmm. Cela semble assez évident. À ce stade, nous devons prendre une décision. Il est possible de continuer cet exercice avec encore plus de magie PHP, mais cette approche transformera, à un moment donné, ce tutoriel en une feuille de calcul PHP au lieu d'un modèle de conception. Alors, prenons la décision d'implémenter les méthodes réelles pour ajouter, soustraire et multiplier des objets d'argent.

fonction testTwoMoneyObjectsCanBeAdded () $ m1 = new Money (100, Devise :: USD ()); $ m2 = nouvel argent (200, monnaie :: USD ()); $ sum = new Money (300, Currency :: USD ()); $ this-> assertEquals ($ sum, $ m1-> add ($ m2)); 

Ce test échoue également, mais avec une erreur nous indiquant qu'il n'y a pas "ajouter" méthode sur Argent.

fonction publique getAmount () return $ this-> montant;  function add ($ autre) retourne une nouvelle monnaie ($ this-> montant + $ autre-> getAmount (), $ this-> devise); 

Pour résumer deux Argent Objets, nous avons besoin d’un moyen de récupérer la quantité de l’objet que nous transmettons en tant qu’argument. Je préfère écrire un getter, mais définir la variable de classe sur public serait également une solution acceptable. Mais si nous voulons ajouter des dollars à l'euro?

/ ** * @expectedException Exception * @expectedExceptionMessage Les deux espèces doivent être de la même devise * / function testItThrowsExceptionIfWeTryToAddTwoMoneysWithDifferentCurrency () $ m1 = new Money (100, Monnaie = USD ()); $ m2 = nouvel argent (100, Monnaie :: EUR ()); $ m1-> add ($ m2); 

Il y a plusieurs façons de traiter les opérations sur Argent objets avec différentes devises. Nous allons lancer une exception et l’attendre dans le test. Alternativement, nous pourrions implémenter un mécanisme de conversion de devise dans notre application, appelez-le, convertir les deux Argent objets dans une devise par défaut et les comparer. Ou, si nous avions un algorithme de conversion de devise plus sophistiqué, nous pourrions toujours convertir de l'un à l'autre et comparer dans cette devise convertie. Le fait est que, lorsque la conversion sera mise en place, les frais de conversion devront être pris en compte et que les choses se compliqueront. Alors jetons simplement cette exception et passons à autre chose..

fonction publique getCurrency () return $ this-> currency;  fonction add (Money $ other) $ this-> EnsureSameCurrencyWith ($ other); renvoyer de nouvelles sommes d'argent ($ this-> montant + $ autre-> getAmount (), $ this-> devise);  fonction privée verifySameCurrencyWith (Money $ other) if ($ this-> devise!! = $ other-> getCurrency ()) lève une nouvelle exception ("Les deux moneys doivent être de la même devise"); 

C'est mieux. Nous vérifions si les devises sont différentes et levons une exception. Je l'ai déjà écrit en tant que méthode privée distincte, car je sais que nous en aurons également besoin dans les autres opérations mathématiques..

La soustraction et la multiplication sont très similaires à l'addition, donc voici le code et vous pouvez trouver les tests dans le code source ci-joint.

fonction soustraire (Money $ autre) $ this-> EnsureSameCurrencyWith ($ other); if ($ other> $ this) lance une nouvelle exception ("L'argent soustrait est supérieur à ce que nous avons"); renvoyer de nouvelles sommes d'argent ($ this-> montant - $ autre-> getAmount (), $ this-> devise);  fonction multiplyBy ($ multiplicateur, $ roundMethod = PHP_ROUND_HALF_UP) $ product = rond ($ this-> montant * $ multiplicateur, 0, $ roundMethod); renvoyer de nouvelles sommes d'argent ($ produit, $ ceci-> devise); 

Avec la soustraction, nous devons nous assurer d’avoir assez d’argent et avec la multiplication, nous devons prendre des mesures pour arrondir les montants de façon à éviter que la division (multiplication avec des nombres inférieurs à un) ne produise pas des "demi-centimes". Nous gardons notre montant en cents, le facteur le plus bas possible de la monnaie. On ne peut plus le partager.


Introduction de la monnaie à notre compte

Nous avons presque complet Argent et Devise. Il est temps de présenter ces objets à Compte. Nous allons commencer avec Devise, et changer nos tests en conséquence.

fonction testItCanHavePrimaryAndSecondaryCurrency () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ this-> account-> setSecondaryCurrency (Currency :: USD ()); $ this-> assertEquals (array ('primary' => Devise :: EUR (), 'secondary' => Devise :: USD ()), $ this-> compte-> getC devises ()); 

En raison de la nature du typage dynamique de PHP, ce test réussit sans problème. Cependant, je voudrais forcer les méthodes dans Compte utiliser Devise objets et ne rien accepter d'autre. Ce n'est pas obligatoire, mais je trouve ces types d'indications de type extrêmement utiles lorsque quelqu'un d'autre a besoin de comprendre notre code..

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

Il est maintenant évident pour quiconque lisant ce code pour la première fois que Compte marche avec Devise.


Introduire de l'argent dans notre compte

Les deux actions de base que tout compte doit fournir sont les suivantes: dépôt - c’est-à-dire ajouter de l’argent à un compte - et retrait - c’est-à-dire retirer de l’argent d’un compte. Le dépôt a une source et le retrait a une destination autre que notre compte courant. Nous n'entrerons pas dans les détails sur la manière de mettre en œuvre ces transactions, nous nous concentrerons uniquement sur les effets qu'elles ont sur notre compte. Nous pouvons donc imaginer un test comme celui-ci pour le dépôt.

fonction testAccountCanDepositMoney () $ this-> compte-> setPrimaryCurrency (Currency :: EUR ()); $ money = new money (100, Currency :: EUR ()); // C'est 1 EURO $ this-> compte-> dépôt ($ argent); $ this-> assertEquals ($ money, $ this-> compte-> getPrimaryBalance ()); 

Cela nous obligera à écrire pas mal de code d'implémentation.

compte classe private $ id; private $ primaryCurrency; private $ secondaryCurrency; private $ secondaryBalance; private $ primaryBalance; fonction getSecondaryBalance () return $ this-> secondaryBalance;  function getPrimaryBalance () return $ this-> primaryBalance;  function __construct ($ id) $ this-> id = $ id;  […] Dépôt de fonction (Money $ money) $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ money: $ this-> secondaryBalance = $ money; 

OK OK. Je sais, j'ai écrit plus que ce qui était absolument nécessaire, pour la production. Mais je ne veux pas vous ennuyer à mort avec des pas de bébé et je suis aussi à peu près sûr que le code pour balance secondaire fonctionnera correctement. Il a été presque entièrement généré par l'IDE. Je vais même sauter de le tester. Bien que ce code rende notre test réussi, nous devons nous demander ce qui se passe lorsque nous effectuons des dépôts ultérieurs. Nous voulons que notre argent soit ajouté au solde précédent.

fonction testSubsequentDepositsAddUpTheMoney () $ this-> compte-> setPrimaryCurrency (Currency :: EUR ()); $ money = new money (100, Currency :: EUR ()); // C'est 1 EURO $ this-> compte-> dépôt ($ argent); // Un euro dans le compte $ this-> compte-> deposit ($ money); // Deux euros dans le compte $ this-> assertEquals ($ money-> multiplyBy (2), $ this-> account-> getPrimaryBalance ()); 

Eh bien, cela échoue. Nous devons donc mettre à jour notre code de production.

Dépôt de fonction (Money $ money) if ($ this-> primaryCurrency == $ money-> getCurrency ()) $ this-> primaryBalance = $ this-> primaryBalance? : new Money (0, $ this-> primaryCurrency); $ this-> primaryBalance = $ this-> primaryBalance-> add ($ money);  else $ this-> secondaryBalance = $ this-> secondaryBalance? : new Money (0, $ this-> secondaryCurrency); $ this-> secondaryBalance = $ this-> secondaryBalance-> add ($ money); 

Ceci est vraiment mieux. Nous en avons probablement fini avec le dépôt méthode et nous pouvons continuer avec se désister.

fonction testAccountCanWithdrawMoneyOfSameCurrency () $ this-> compte-> setPrimaryCurrency (Currency :: EUR ()); $ money = new money (100, Currency :: EUR ()); // C'est 1 EURO $ this-> compte-> dépôt ($ argent); $ this-> compte-> retirez (nouvelle monnaie (70, devise :: euros ())); $ this-> assertEquals (nouvelle monnaie (30, devise :: EUR ()), $ this-> compte-> getPrimaryBalance ()); 

Ceci est juste un test simple. La solution est simple, aussi.

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

Eh bien, cela fonctionne, mais si nous voulons utiliser un Devise ce n'est pas dans notre compte? Nous devrions lancer une excuse pour cela.

/ ** * @expectedException Exception * @expectedExceptionMessage Ce compte n'a pas de devise USD * / function testThrowsExceptionForInexistentCurrencyOnWithdraw () $ this-> account-> setPrimaryCurrency (Currency :: EUR ()); $ money = new money (100, Currency :: EUR ()); // C'est 1 EURO $ this-> compte-> dépôt ($ argent); $ this-> compte-> retirer (nouvelle monnaie (70, monnaie :: USD ())); 

Cela nous obligera également à vérifier nos devises.

function retire (Money $ money) $ this-> validateCurrencyFor ($ money); $ this-> primaryCurrency == $ money-> getCurrency ()? $ this-> primaryBalance = $ this-> primaryBalance-> soustraire ($ money): $ this-> secondaryBalance = $ this-> secondaryBalance-> soustraire ($ money);  fonction privée validateCurrencyFor (Money $ money) if (! in_array ($ money-> getCurrency (), $ this-> getCurrency ())) lance une nouvelle exception (sprintf ('Ce compte n'a aucune devise% s', $ money -> getCurrency () -> getStringRepresentation ())); 

Mais que faire si nous voulons retirer plus que ce que nous avons? Ce cas a déjà été abordé lorsque nous avons mis en œuvre la soustraction sur Argent. Voici le test qui le prouve.

/ ** * @expectedException Exception * @expectedExceptionMessage L'argent soustrait est plus que ce que nous avons * / function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> set-> setPrimaryCurrency (Currency :: EUR ()); $ money = new money (100, Currency :: EUR ()); // C'est 1 EURO $ this-> compte-> dépôt ($ argent); $ this-> account-> retirez (nouvelle monnaie (150, devise :: euros ())); 

Traiter avec le retrait et l'échange

L'un des problèmes les plus difficiles à résoudre lorsque nous travaillons avec plusieurs devises est l'échange entre elles. La beauté de ce modèle de conception réside dans le fait qu’il nous permet de simplifier un peu ce problème en l’isolant et en l’encapsulant dans sa propre classe. Alors que la logique dans un Échange classe peut être très sophistiqué, son utilisation devient beaucoup plus facile. Par souci de ce tutoriel, imaginons que nous ayons des bases très simples Échange logique seulement. 1 EUR = 1,5 USD.

classe Exchange function convert (Money $ money, Monnaie $ toCurrency) if ($ toCurrency == Monnaie :: EUR () && $ money-> getCurrency () == Monnaie :: USD ()) renvoie un nouvel argent ($ money -> multiplyBy (0.67) -> getAmount (), $ toCurrency); if ($ toCurrency == Monnaie :: USD () && $ money-> getCurrency () == Monnaie :: EUR ()) renvoie un nouvel argent ($ money-> multiplyBy (1.5) -> getAmount (), $ toCurrency) ; retourner $ argent; 

Si nous convertissons d'EUR en USD, nous multiplions la valeur par 1,5. Si nous convertissons d'USD en EUR, nous divisons la valeur par 1,5. Sinon, nous supposons que nous convertissons deux devises du même type. Nous ne faisons donc rien et ne reversons que l'argent. . Bien sûr, en réalité ce serait une classe beaucoup plus compliquée.

Maintenant, avoir un Échange classe, Compte peut prendre des décisions différentes lorsque nous voulons nous retirer Argent dans une devise, mais nous n’avons pas assez de pouvoir dans cette devise spécifique. Voici un test qui l'illustre mieux.

fonction testItConvertsMoneyFromTheOtherCurrencyWhenWeDoNotHaveEnoughInTheCurrentOne () $ this-> account-> setPrimaryCurrency (Currency :: USD ()); $ money = new money (100, Currency :: USD ()); // Cela fait 1 USD $ this-> compte-> dépôt ($ argent); $ this-> account-> setSecondaryCurrency (Currency :: EUR ()); $ money = new money (100, Currency :: EUR ()); // C'est 1 EURO = 1,5 USD $ this-> compte-> dépôt ($ argent); $ this-> compte-> retirez (nouvelle monnaie (200, devise :: USD ())); // Cela fait 2 USD $ ceci-> assertEquals (nouvelle monnaie (0, devise :: USD ()), $ ceci-> compte-> getPrimaryBalance ()); $ this-> assertEquals (nouvelle monnaie (34, devise :: EUR ()), $ this-> compte-> getSecondaryBalance ()); 

Nous définissons la devise principale de notre compte sur USD et déposons un dollar. Ensuite, nous définissons la devise secondaire en EUR et déposons un euro. Ensuite, nous retirons deux dollars. Enfin, nous prévoyons de rester avec zéro dollar et 0,34 euro. Bien sûr, ce test lève une exception, nous devons donc mettre en œuvre une solution à ce dilemme.

function retire (Money $ money) $ this-> validateCurrencyFor ($ money); if ($ this-> primaryCurrency == $ money-> getCurrency ()) if ($ this-> primaryBalance> = $ money) $ this-> primaryBalance = $ this-> primaryBalance-> soustraire ($ money);  else $ ourMoney = $ this-> primaryBalance-> add ($ this-> secondaryToPrimary ()); $ argent restant = $ notreMonnaie-> soustraire ($ argent); $ this-> primaryBalance = new Money (0, $ this-> primaryCurrency); $ this-> secondaryBalance = (new Exchange ()) -> convertir ($ resteMoney, $ this-> secondaryCurrency);  else $ this-> secondaryBalance = $ this-> secondaryBalance-> subtract ($ money);  fonction privée secondaryToPrimary () return (new Exchange ()) -> convert ($ this-> secondaryBalance, $ this-> primaryCurrency); 

Wow, beaucoup de modifications ont dû être apportées pour prendre en charge cette conversion automatique. En réalité, si nous extrayons de notre devise principale et que nous n’avons pas assez d’argent, nous convertissons notre solde de devise secondaire en devise principale et essayons à nouveau la soustraction. Si nous n’avons toujours pas assez d’argent, le $ notre argent object lancera l'exception appropriée. Sinon, nous mettrons à zéro notre solde primaire, nous convertirons l’argent restant en devise secondaire et établirons notre solde secondaire à cette valeur..

Il reste à la logique de notre compte d'implémenter une conversion automatique similaire pour la devise secondaire. Nous ne mettrons pas en place une telle logique symétrique. Si vous aimez l'idée, considérez-la comme un exercice pour vous. Pensez également à une méthode privée plus générique qui ferait la magie de la conversion automatique dans les deux cas..

Ce changement complexe de notre logique nous oblige également à mettre à jour un autre de nos tests. Chaque fois que nous voulons convertir automatiquement, nous devons avoir un solde, même s'il est juste nul.

/ ** * @expectedException Exception * @expectedExceptionMessage L'argent soustrait est plus que ce que nous avons * / function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave () $ this-> account-> set-> setPrimaryCurrency (Currency :: EUR ()); $ money = new money (100, Currency :: EUR ()); // C'est 1 EURO $ this-> compte-> dépôt ($ argent); $ this-> account-> setSecondaryCurrency (Currency :: USD ()); $ money = new Money (0, Currency :: USD ()); $ this-> compte-> dépôt ($ argent); $ this-> account-> retirez (nouvelle monnaie (150, devise :: euros ())); 

Allouer de l'argent entre les comptes

La dernière méthode que nous devons mettre en œuvre sur Argent est allouer. C'est la logique qui décide quoi faire lorsque l'on divise de l'argent entre différents comptes, ce qui ne peut pas être fait exactement. Par exemple, si nous avons 0,10 cent et que nous voulons les répartir entre deux comptes dans une proportion de 30 à 70%, c'est facile. Un compte recevra trois cents et les sept autres. Cependant, si nous voulons faire la même allocation du ratio 30-70 de cinq cents, nous avons un problème. L'allocation exacte serait de 1,5 cent sur un compte et de 3,5 sur l'autre. Mais nous ne pouvons pas diviser les cents, nous devons donc mettre en œuvre notre propre algorithme pour allouer les fonds.

Il peut y avoir plusieurs solutions à ce problème, un algorithme courant consiste à ajouter un cent séquentiellement à chaque compte. Si un compte a plus de centimes que sa valeur mathématique exacte, il doit être supprimé de la liste de répartition et ne plus recevoir d’argent. Voici une représentation graphique.


Et un test pour prouver notre point est ci-dessous.

fonction testItCanAllocateMoneyBetween2Accounts () $ a1 = $ this-> anAccount (); $ a2 = $ this-> anAccount (); $ money = new money (5, Currency :: USD ()); $ argent-> allouer (a1, a2, 30, 70 $); $ this-> assertEquals (nouvelle monnaie (2, devise :: USD ()), $ a1-> getPrimaryBalance ()); $ this-> assertEquals (nouvelle monnaie (3, devise :: USD ()), $ a2-> getPrimaryBalance ());  fonction privée anAccount () $ account = new Account (1); $ compte-> setPrimaryCurrency (Currency :: USD ()); $ compte-> dépôt (argent neuf (0, monnaie :: USD ())); retourner le compte $; 

Nous venons de créer un Argent objet avec cinq cents et deux comptes. Nous appelons allouer et attendez-vous à ce que les deux ou trois valeurs soient dans les deux comptes. Nous avons également créé une méthode d'assistance pour créer rapidement des comptes. Le test échoue, comme prévu, mais nous pouvons le faire passer assez facilement.

fonction allocate (compte $ a1, compte a2, $ a1 pour cent, a2 pour cent) $ exactA1Balance = $ this-> montant * $ a1 pour cent / 100; $ exactA2Balance = $ this-> montant * $ a2Percent / 100; $ oneCent = new Money (1, $ this-> devise); while ($ this-> montant> 0) si ($ a1-> getPrimaryBalance () -> getAmount () < $exactA1Balance)  $a1->dépôt (un cent); $ this-> amount--;  if ($ this-> montant <= 0) break; if ($a2->getPrimaryBalance () -> getAmount () < $exactA2Balance)  $a2->dépôt (un cent); $ this-> amount--; 

Bien, pas le code le plus simple, mais il fonctionne correctement, comme le prouve le succès de notre test. La seule chose que nous puissions encore faire à ce code est de réduire la petite duplication à l’intérieur du fichier. tandis que boucle.

fonction allocate (compte $ a1, compte a2, $ a1 pour cent, a2 pour cent) $ exactA1Balance = $ this-> montant * $ a1 pour cent / 100; $ exactA2Balance = $ this-> montant * $ a2Percent / 100; while ($ this-> montant> 0) $ this-> allocateTo ($ a1, $ exactA1Balance); si ($ this-> montant <= 0) break; $this->allocateTo ($ a2, $ exactA2Balance);  fonction privée allocateTo ($ compte, $ exactBalance) if ($ compte-> getPrimaryBalance () -> getAmount () < $exactBalance)  $account->dépôt (nouvel argent (1, $ this-> devise)); $ this-> amount--; 

Dernières pensées

Ce que je trouve étonnant avec ce petit motif est le grand nombre de cas où nous pouvons l’appliquer.

Nous avons fini avec notre modèle d'argent. Nous avons vu qu’il s’agissait d’un modèle assez simple, qui résume les spécificités du concept d’argent. Nous avons également vu que cette encapsulation allège le fardeau des calculs de Account. Compte peut se concentrer sur la représentation du concept à un niveau supérieur, du point de vue de la banque. Le compte peut implémenter des méthodes telles que la connexion avec les titulaires de compte, les identifiants, les transactions et l'argent. Ce sera un orchestrateur, pas une calculatrice. L'argent prendra en charge les calculs.

Ce que je trouve étonnant avec ce petit motif est le grand nombre de cas où nous pouvons l’appliquer. En gros, chaque fois que vous avez une paire valeur-unité, vous pouvez l'utiliser. Imaginez que vous ayez une application météo et que vous souhaitiez implémenter une représentation de la température. Ce serait l'équivalent de notre objet Money. Vous pouvez utiliser Fahrenheit ou Celsius comme monnaie.

Un autre cas d'utilisation est lorsque vous avez une application de cartographie et que vous souhaitez représenter des distances entre des points. Vous pouvez facilement utiliser ce modèle pour basculer entre les mesures métriques et les mesures impériales. Lorsque vous travaillez avec des unités simples, vous pouvez supprimer l'objet Exchange et implémenter la logique de conversion simple dans votre objet "Money"..

J'espère donc que vous avez apprécié ce tutoriel et que j'ai hâte de connaître les différentes manières dont vous pourriez utiliser ce concept. Merci pour la lecture.