Aproximación al patrón Money
Martin Fowler introdujo hace ya un tiempo la representación de valores monetarios con lo que denominaríamos como Money pattern o el patrón Money.
Este patrón lo podemos encontrar también en el libro de Martin Fowler Patterns of Enterprise Application Architecture [PEAA].
Es bastante habitual que en determinados contextos de desarrollo, sea necesario utilizar monedas/dinero.
El problema es que no existe un tipo de datos que nos permita definir el tipo de dato «dinero».
La consecuencia de esto, es que nos podemos encontrar con errores a la hora de realizar cálculos entre diferentes monedas, sobre todo los relacionados con los redondeos.
A continuación y sin ser purista plenamente, voy a exponer un acercamiento a este patrón.
Dejo de lado en esta entrada aspectos que tendrían que ver con la creación genérica de un objeto de tipo moneda asumiendo inicialmente que es un tipo de datos decimal, la comparación de dos objetos de tipo moneda, y las posibles operaciones o cálculos entre diferentes objetos de tipo moneda, ya que éstos además de deber ser el mismo tipo de moneda, deberían permitir las operaciones básicas entre ellas, es decir, la suma, resta, producto, división, etc.
Así que siendo un poco simplista respecto al patrón, una aproximación al mismo como tal quedaría de la siguiente forma que voy a exponer, sin entrar en algunos de estos tipos de detalles más profundos que requerirían de mayor profundidad de explicación y discusión.
Para el ejemplo de aplicación de un posible patrón Money, voy a aplicar también el patrón Type-Safe Enum para evitar el uso de enums.
Las clases que voy a crear son los que se detallan en la siguiente imagen:
Como podemos apreciar en la imagen, voy a crear una clase CurrencyOf que utilizará el patrón Type-Safe Enum.
La clase es un poco extensa, por lo que sólo voy a poner una pequeña parte de la misma, pero lo que sí diré es que posee todos los códigos ISO 4217 para monedas.
namespace MoneyPattern { using System; using System.Collections.Generic; using System.Linq; /// <summary> /// ISO 4217 currency code and numeric code. /// </summary> public abstract class CurrencyOf { public static readonly CurrencyOf AedType = new AED(); public static readonly CurrencyOf AfnType = new AFN(); /// more here public static readonly CurrencyOf ZwlType = new ZWL(); private readonly string _key; public string Key => _key; private readonly string _value; public string Value => _value; private CurrencyOf(string name, string isoCode) { this._key = isoCode; this._value = name; } public static IReadOnlyCollection<CurrencyOf> GetAll() { return new[] { AedType }; } private static T ThrowException<T>() { throw new ArgumentOutOfRangeException(); } public static CurrencyOf FindBy(string isoCode) { return GetAll().SingleOrDefault(s => s.Key == isoCode) ?? ThrowException<CurrencyOf>(); } private class AED : CurrencyOf { public AED() : base(nameof(AED), "784") { } } private class AFN : CurrencyOf { public AFN() : base(nameof(AFN), "971") { } } // more here private class ZWL : CurrencyOf { public ZWL() : base(nameof(ZWL), "932") { } } } }
Por su parte, la clase Money es la que contiene no sólo la cantidad sino también el tipo de moneda.
Es la clave en esta aproximación que estoy haciendo del patrón Money.
El código de esta clase es el que se indica a continuación:
namespace MoneyPattern { public sealed class Money { private readonly decimal _amount; public decimal Amount => _amount; private readonly CurrencyOf _currencyType; public CurrencyOf CurrencyType => _currencyType; public Money(decimal amount, CurrencyOf currencyType) { _amount = amount; _currencyType = currencyType; } } }
Como algo adicional, he credo una extensión que nos permita transformar una clase Money a otra clase Money. Esta clase la he llamado MoneyExtensions.
El código de esta clase es el que se indica a continuación:
namespace MoneyPattern { using System; public static class MoneyExtensions { public static Money ConvertToMoney(this Money fromMoney, CurrencyOf toCurrencyType, decimal exchangeRate) { if (fromMoney == null) throw new ArgumentNullException(nameof(fromMoney)); return new Money(fromMoney.Amount * exchangeRate, toCurrencyType); } } }
Fuera del patrón en sí, he creado una clase de nombre Product que hará las acciones de almacenar una marca, un producto, y una moneda.
Imaginemos esto como un carrito donde vamos a pagar por un producto con su moneda.
Aunque algo simple, servirá para poner en práctica este patrón como veremos, pero lo lógico aquí sería pensar en una colección de productos y una moneda para todos ellos, aunque aquí lo estoy simplificando.
El código de esta clase quedará de la siguiente forma:
namespace MoneyPattern { public class Product { private readonly string _trademark; public string Trademark => _trademark; private readonly string _name; public string Name => _name; private readonly Money _money; public Money Money => _money; public Product(string trademark, string name, Money money) { _trademark = trademark; _name = name; _money = money; } } }
Y ya por último, la aplicación de consola que ejecutará esta acción y cuyo código corresponde con el que se indica a continuación dentro de la clase Program.
namespace MoneyPattern { using System; public class Program { public static void Main(string[] args) { Console.WriteLine("Change types"); Console.ForegroundColor = ConsoleColor.Cyan; decimal amountChangeEurToUsd = 1.19245m; Console.WriteLine($"EUR to USD => {amountChangeEurToUsd}"); decimal amountChangeEurToGbp = 0.915578931m; Console.WriteLine($"EUR to GBP => {amountChangeEurToGbp}"); Console.WriteLine(); Console.ForegroundColor = ConsoleColor.White; Money moneySpain = new Money(749m, CurrencyOf.EurType); Product product = new Product("Microsoft", "Surface Pro 4", moneySpain); ProductToString(product); Money moneyUnitedStates = moneySpain.ConvertToMoney(CurrencyOf.UsdType, amountChangeEurToUsd); product = new Product(product.Trademark, product.Name, moneyUnitedStates); ProductToString(product); Money moneyBritish = moneySpain.ConvertToMoney(CurrencyOf.GbpType, amountChangeEurToGbp); product = new Product(product.Trademark, product.Name, moneyBritish); ProductToString(product); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(); Console.WriteLine("Press any key to close"); Console.ReadKey(); } private static void ProductToString(Product product) { Console.WriteLine($"{product.Trademark} {product.Name} (ISO 4217 => {product.Money.CurrencyType.Key}) {product.Money.CurrencyType.Value} {product.Money.Amount}"); } } }
Nuestra aplicación en ejecución queda de la siguiente forma:
Aunque es una aproximación al patrón Money y hay muchos purismos alrededor de él, incluso si usar decimal u otras técnicas, o como por ejemplo el que se indica aquí, hay que tener en cuenta que lo importante es controlar los cambios de moneda, los cálculos con los mismos sin perder precisión, y poder pasar de una a otra moneda ni hacer cálculos sin tener que sufrir problemas colaterales por esto.
Si quieres acceder al código fuente (C#) de esta aproximación del patrón, te invido a acceder a este enlace.
¡Happy Coding!