Colabora |
El patrón MonedaMuchas aplicaciones manejan dinero, pero en .NET no existe el Dinero como un Tipo básico. He escrito una estructura llamada Dinero que intenta resolver este problema.
Fecha: 05/Sep/2008 (04-09-08)
|
IntroducciónEste verano he estado mirando con detenimiento los patrones de diseño de software "Command" y "Memento" y buscando información por la Red me he encontrado con el Patrón Money, me ha llamado la atención y me he dedicado a investigar sobre este asunto y este documento es el resultado de esa curiosidad Martin Fowler[http://martinfowler.com/] la persona que describió los primeros patrones de diseño, describe en su libro Analysis Patterns el patrón Quantity y en su libro en Patterns of Enterprise Application Architecture [PEAA] describe el patrón Money, entre otros muchos patrones empresariales [Véase http://www.martinfowler.com/eaaCatalog/index.html documento que contiene una relación de patrones empresariales]y dice (más o menos) lo siguiente: Una gran cantidad de ordenadores trabajan manipulando dinero, y ningún ¿lenguaje? / ¿Sistema? de la actualidad contiene una clase o un tipo que defina el dinero. Esta falta causa problemas ente los que se encuentran los problemas de redondeo de cifras en las operaciones de cambio de divisas. Lo bueno que tiene la programación orientada a objetos es que se pueden solventar esos problemas creando una clase dinero [Money, en el original] que resuelva esos problemas p.488. (Traducción bastante libre)
[TOC] Tabla de Contenidos
Uso del códigoUsar este tipo "Dinero" es muy fácil, se puede usar como cualquier otro tipo básico Dim d1 As d1 = 25.04D Dim d2 As Dinero = 35.25D Dim sumas As Dinero = d1 + d2 Dim diferencia As Dinero = d1 - d2
Constructores del Tipo MonedaLos constructores que he definido para la estructura son los siguientes: El constructor Copia Public Sub New(Dinero) Para cargar números decimales Public Sub New(Decimal) Public Sub New(Decimal, String) Public Sub New(Decimal, System.Globalization.CultureInfo) Para cargar números enteros Public Sub New(Long) Public Sub New(Long, String) Public Sub New(Long, System.Globalization.CultureInfo) En los constructores, el valor [String] es una cadena con el nombre de una cultura en el formato [códigoidioma2]-[códigopaís/región2]. Donde:
El constructor estándar
Dim EurosEspaña = New Dinero(129.56D) Crea el dinero en la moneda de la cultura donde se está trabajando, es decir, si estamos en España la moneda serán euros, y si estamos en EEUU la moneda serán dólares. Pero existen constructores que permiten crear una moneda determinada, por ejemplo podemos crear una moneda en dólares Dim DolaresUS = New Dinero(129.56D, "en-US") O bien Dim DolaresUS = New Dinero(129.56D,New System.Globalization.CultureInfo("en-US")) O podemos crear la moneda que usan los italianos (euros, evidentemente) Dim EurosIT = New Dinero(129.56D, "it-IT") O bien Dim EurosIT = New Dinero(129.56D, New System.Globalization.CultureInfo("it-IT"))
Comentarios sobre el diseño
Porque una estructuraBueno, no hay una razón muy definida, las implementaciones que he consultado, tampoco se ponen de acuerdo y hay para todos los gustos
Pero después de pensar un poco, he decidido usar un tipo de valor, porque lo que estoy haciendo es guardar una cantidad de dinero, por lo que por similitud lo lógico es emplear un tipo de valor. Por esta razón he utilizado una estructura para definir este nuevo tipo
Diseño BásicoEl diseño básico es el siguiente: Public Structure Dinero '-------------------------------------------------------------- ''' <summary>Parte entera del número</summary> Private _ParteEntera As Long '-------------------------------------------------------------- ''' <summary>Parte decimal del número</summary> Private _ParteDecimal As Integer '-------------------------------------------------------------- ''' <summary> ''' Referencia cultural de la que se obtiene ''' la forma de usar los valores monetarios ''' </summary> Private _CultureInfo As System.Globalization.CultureInfo End Structure
Una reflexión sobre los tipos de coma flotantePara informarme de los posibles problemas de manipular dinero he estado leyendo artículos que hablan de estos problemas de redondeo, Los más destacados que he encontrado son los siguientes:
Después de leerlos con detenimiento, tengo que reconocer que he sido incapaz de reproducir en Visual Basic .NET, los problemas que describen esos documentos, así que una de dos, o soy un bestia (que todo puede ser) o la plataforma .NET no tiene esos problemas (que también puede ser, aunque no puede ser tan buena ¿no?). Aunque sí que he visto es que existen problemas de pérdida de números (sobre todo las últimas cifras decimales) cuando se cambia de formato por ejemplo de Double a Decimal (y al revés). La persistencia en la Red, de la idea de que existe un problema de redondeo puede ser debida a problemas reales que existen en algunos lenguajes, pero, también puede deberse a que son documentos viejos, y están hablando de tipos (¿float?) de lenguajes como "C" (me refiero al C puro y/o tradicional) y tipos parecidos en Java o JavaScript. Bien, después de estudiar el problema, he tomado (más bien copiado) la solución que implementa codekaizen que consiste en guardar la cantidad de dinero en dos variables separadas. La parte entera se guarda en una variable de tipo Long, (lo que supone 19 posiciones numéricas) y la parte decimal en una variable del tipo Integer, conservando hasta nueve cifras decimales. De esta forma obtengo dos beneficios, por una parte, en el caso de que realmente existan los problemas de redondeo, los evito, y por otra parte conservo suficiente información decimal para que los problemas que puedan existir en las operaciones con dinero se minimicen (por ejemplo: conversiones, sumas, multiplicar por [0,14] para obtener el IVA, etc.). Si el número tiene más decimales se truncan y solo se conservan los nueve primeros. Resumiendo, el rango máximo de valores numéricos con los que se puede operar son Parte entera del numero entre -9.223.372.036.854.775.808 y 9.223.372.036.854.775.807 (9,2... E+18). Y la parte decimal del número siempre con nueve decimales
Como defino la monedaUna curiosidad: He encontrado una norma ISO (ISO 4217) que define y denomina a todas las monedas del mundo Viendo las implementaciones de otros autores, uno se da cuenta que realmente éste es el autentico problema de esta estructura y una de las decisiones de diseño más importantes En Primer lugar, no se pueden realizar operaciones entre monedas, (por ejemplo una suma) si las monedas no son del mismo tipo, por ejemplo, no tiene sentido sumar dólares y euros. (Aunque si lo tiene realizar operaciones de conversión entre monedas) En segundo lugar, tenemos que disponer de un montón de información específica de la moneda, como puede ser, el nombre (Euro), el símbolo de la moneda (€), el numero de decimales con los que trabaja la moneda (Euros dos decimales), el símbolo que se usa como separador decimal (,) el separador de los millares (.), el formato de los números negativos, etc. etc. La pregunta del millón es: ¿cómo manejo toda esta información? Existen varias alternativas, (cada una de las implementaciones consultadas utiliza una diferente), una de ellas utiliza clases auxiliares para mantener toda esa información de la moneda. Otra utiliza variables estáticas con los nombres de las monedas. Pero después de haber leído y mirado, me parece que todo el problema se resuelve utilizando la clase System.Globalization.CultureInfo y guardando su valor en un campo de la estructura, porque una vez definida una cultura por ejemplo la española System.Globalization.CultureInfo("es-ES"), Puedo acceder a todos los datos de esa cultura. Con el nombre de la cultura, puedo definir la clase System.Globalization.RegionInfo("es-ES") y acceder a todos los datos correspondientes a la región en este caso (España) Y lo más interesante, a través de la clase NumberFormatInfo puedo acceder a todos los datos que necesito conocer de la moneda (nombre símbolo, posiciones decimales etc. Dim cultura As New Globalization.CultureInfo("es-ES") Dim formNum As System.Globalization.NumberFormatInfo formNum = cultura.NumberFormat Es decir, utilizado solo una variable en la estructura, puedo acceder a toda la información que necesito sobre la moneda. De esta forma obtengo varios beneficios, el primero es que no necesito un montón de campos en la estructura para almacenar toda esa información, y por lo tanto, tampoco necesito cargarlos (por ejemplo a través del constructor) para empezar a trabajar con esos datos Otros autores no emplean esta estrategia y esgrimen varias razones:
De todas formas, y quitando el ultimo inconveniente expuesto, la realidad es que en .NET, guardando la información de la "Cultura" se pueden resolver todos los problemas asociados a la información de la moneda de una forma sencilla (y elegante) En la estructura existen una serie de funciones que sirven para acceder a la información que se necesita de la moneda, a través de las clases CultureInfo, RegionInfo, y NumberFormatInfo La información de la cultura se carga en el momento de crear el dinero y luego no se puede modificar. Para proporcionar a la estructura la información de la cultura con al que se va a trabajar, existen dos maneras, la primera es proporcionar un objeto del tipo [System.Globalization.CultureInfo], la segunda es proporcionar una cadena que contenga el nombre de la cultura en el formato [name] que son 5 caracteres, por ejemplo [es-ES], [en-GB], [en-US]. Para evitar errores, existe una función cuya firma es: Private Shared Function ControlExistenciaCultura(String) As String Que se encarga de comprobar que la cadena que define la cultura tenga el formato correcto y además exista
Operaciones aritméticasEn las operaciones entre monedas, (por ejemplo, sumas o restas), hay que operar con la misma moneda, no tiene sentido hacer operaciones de suma de dinero entre Euros (€) y Dólares ($) , por eso, en las funciones que realizan operaciones aritméticas, lo primero que se comprueba es que las monedas sean las mismas independientemente del país o de la región, (euros de España o de Italia) antes de permitir operaciones aritméticas. Private Shared Sub ComprobacionMismaMoneda( _ ByVal d1 As Dinero, _ ByVal d2 As Dinero) If d1.MonedaIsoCurrencySymbol <> d2.MonedaIsoCurrencySymbol Then Throw New InvalidOperationException( _ "Las monedas son diferentes") End If End Sub La función [MonedaIsoCurrencySymbol] Obtiene el símbolo de moneda ISO 4217 de tres caracteres asociado al país o región. (Por ejemplo EGY de Egipto, ESP de España, FIN de Finlandia) Una de las decisiones de diseño más importantes es guardar las cantidades de dinero en dos variables independiente, en una (de tipo Long) guardo la parte entera de la cantidad, y en otra (de tipo Integer) guardo la parte decimal con nueve decimales. Esta forma de guardar las cantidades tiene un pequeño problema, y es el siguiente: ¿Como realizo las operaciones aritméticas? En realidad no es difícil ya que solo hay dos opciones:
Cuáles son las ventajas e inconvenientes:
Las operaciones aritméticas se realizan sin unir las cantidades es decir operando con un numero de la forma (A+X) Siendo (A) la parte entera del numero y (X) la parte decimal. Por ejemplo, el número 4,25 es (4 + 0,25) Para explicar cómo se hacen las operaciones con los números partidos vamos a recordar un poco de matemáticas
La sumaR=(A+X) + (B+Y) R=(A+B) + (X+Y) Ejemplo R= (5,4) + (6,8) = 12,20 Operaciones R= (5+0,4) + (6+0,8) R= (5+6) + (0.4+0,8) R= (11) + (1,2) = 12,20
La MultiplicaciónR=(A + X) x (B + Y) R= (A x B) + (A x Y) + (X x B) + (X x Y) Ejemplo R= (4,2) x (5,6) = 23,52 Operaciones R= (4 + 0,2) x (5 + 0,6) R= (4 x 5) + (4 x 0,6) + (0,2 x 5) + (0,2 x 0,6) R= (20) + (2,4) + (1) + (0.12) = 23,52
La divisiónR=(A+X) / (B+Y) Si multiplicamos los dos términos de una fracción por un mismo número, la fracción no varia (A+X) x (B+Y) (A x B) + (A x Y) + (X x B) + (X x Y) = ------------------------ = -------------------------------------------------- (B+Y) x (B+Y) (B x B) + (B x Y) + (Y x B) + (Y x Y) (A x B) + (A x Y) + (X x B) + (X x Y) = ------------------------------------------ (B x B) + (2 x B x Y) + (Y x Y) Ejemplo R= (4,2) / (5,6) = 0,75 Operaciones R= (4+0,2) / (5+0,6) (4 + 0,2) x (5+0,6) (4 x 5) + (4 x 0,6) + (0,2 x 5) + (0,2 x 0,6) = ------------------------ = ------------------------------------------------------------ (5 + 0,6) x (5 + 0,6) (5 x 5) + (5 x 0,6) + (5 x 0,6) + (0,6 x 0,6) (20) + (2,4) + (1) + (0.12) 23,52 = ---------------------------------- = --------------- = 0,75 (25) + (3) + (3) + (0,36) 31,36
RestasPara restar dos números, solo tengo que sumarlos con el signo cambiado Es decir R= (A + X) - (B + Y) R= (A + X) + (- (B + Y))
Las conversiones explicitas e implícitasUna de las cosas que más me ha costado entender ha sido el tema de las conversiones implícitas y explicitas, hasta que decidí mirar este tema en la estructura System.Decimal, y entonces se me hizo la Luz ;-) Estas funciones manejan la conversión de valores, y definiéndolas se pueden restringir algún tipo de conversiones, por ejemplo, no tiene sentido convertir una fecha en dinero y/o viceversa ''' <summary> ''' [NO SOPORTADO] Esta conversion no es posible ''' </summary> Public Shared Narrowing Operator CType(ByVal value As Date) As Dinero Throw New System. InvalidCastException ( _ "Esta conversion no es posible") End Operator ''' <summary> ''' [NO SOPORTADO] Esta conversion no es posible ''' </summary> Public Shared Narrowing Operator CType(ByVal value As Dinero) As Date Throw New System. InvalidCastException ( _ "Esta conversion no es posible") End Operator Por ejemplo, no quiero tener problemas con los valores numéricos de coma flotante, y para ello permito que una cantidad de dinero si pueda convertirse en un valor de coma flotante, pero al revés no. Las funciones CType quedaran de la siguiente forma: ''' <summary> ''' Conversion de una cantidad de dinero en un numero Double ''' </summary> Public Shared Narrowing Operator CType(ByVal value As Dinero) As Double Return CType(value.Valor, Double) End Operator ''' <summary> ''' [NO SOPORTADO] No se permite la conversion de un número ''' de punto flotante de precisión doble [Double]en un valor Dinero. ''' Esta conversion no es posible por problemas de redondeo ''' de cantidades, utiliza en su lugar un valor [Decimal] ''' </summary> Public Shared Narrowing Operator CType(ByVal value As Double) As Dinero Using SW As New System.IO.StringWriter( _ System.Globalization.CultureInfo.CurrentCulture) SW.WriteLine("Estructura Dinero") SW.WriteLine("El valor de punto flotante [System.Double][" & value & "] no puede ") SW.WriteLine("ser una cantidad de dinero por problemas de redondeo de cantidades ") SW.WriteLine("Utiliza en su lugar un valor numerico de tipo [System.Decimal] ") SW.Flush() Throw New System.InvalidCastException(SW.ToString) End Using End Operator Mas Información en:
El método ParseNo hay ninguna interface que implemente las funciones Parse() y TryParse(). Pero he mirado la documentación MSDN referente a l tipo System.Decimal, y he copiado su fiema, de forma que la utilización de estas funciones en este nuevo tipo no presente ninguna diferencia con los tipos primitivos de .NET Public Function Parse(String) As Dinero Public Function Parse(String, NumberStyles) As Dinero Public Function Parse(String, IFormatProvider) As Dinero Public Function Parse(String, NumberStyles,IFormatProvider) As Dinero Public Function TryParse(String, Dinero) As Boolean Public Function TryParse(String,NumberStyles,IFormatProvider,Dinero) As System.Boolean
Interfaces SoportadosEsta estructura implementa las siguientes interfaces
Implements ICloneablePara permitir una copia profunda de esta estructura he implementado la interfaz ICloneable Más información en:
Implements IEquatable(Of Dinero)Para poder comparar dos cantidades de Dinero, Hay que implementar la interfaz genérica IEquatable(Of Dinero) que define el método Equals para determinar la igualdad, o no, de dos instancias. Más información en:
Interfaz IComparableDefine un método para comparar tipos de valor (por ejemplo estructuras) o clases Esta interfaz deben implementarla todos aquellos objetos cuyos valores se pueden ordenar, como por ejemplo, las clases numéricas o de tipo cadena. El tipo IComparable expone el miembro: CompareTo para comparar la instancia actual con otro objeto del mismo tipo. El resultado de la operación de comparación es un número con signo que indica los valores relativos de los elementos que se comparan. Por ejemplo si se comparan los objetos D1 y D2 el resultado puede ser -1 si D1 < D2 0 si D1 = D2 +1 si D1 > D2
Implements IComparableDefine un método de comparación generalizado, implementado por un tipo de valor o clase para crear un método de comparación específico del tipo. Más información en:
Implements IComparable(Of Dinero)Define un método de comparación generalizado, implementado por un tipo de valor o clase con el fin de crear un método de comparación específico del tipo para ordenar instancias. Más información en:
Implements IFormattable.NET sugiere implementar la interfaz IFormattable para implementar las funciones ToString() que proporcionan funcionalidad para dar formato al valor de un objeto en una representación de cadena. Un formato describe la apariencia de un objeto cuando se convierte en una cadena. Más información en:
La interfaz se implementa con las siguientes funciones: Public Overrides Function ToString() As String Public Function ToString(format As String) As String Public Function ToString(format As String,IFormatProvider) As String _ Implements System.IFormattable.ToString Public Function ToString(IFormatProvider) As String _ Implements System.IConvertible.ToString El método que se muestra a continuación es el método más usado para obtener el valor Dinero formateado según se indique en el FormatNumber de CultureInfo. La "C" especifica Moneda y a través de CultureInfo se obtienen el separador decimal y de millares, el numero de decimales y el símbolo de la moneda. Public Overloads Overrides Function ToString() As String If Me.CultureInfo Is Nothing Then Return String.Format( _ System.Globalization.CultureInfo.CurrentCulture, _ "{0:C}", _ Me.Valor) Else Return String.Format(Me.CultureInfo, "{0:C}", Me.Valor) End If End Function En la siguiente función el parámetro [format] es una cadena que especifica un formato de impresión
Public Overloads Function ToString(ByVal format As String) As String 'Return Me.Valor.ToString(format) If String.IsNullOrEmpty(format) = True Then format = "{0:C}" End If Return String.Format(format, Me.Valor) End Function La siguiente es la función que implementa la interfaz IFormattable. En ella, el parametro [Format] es una cadena que especifica un formato de impresión. Y [formatProvider] admite cualquier objeto qué implemente la interfaz IformatProvider como CultureInfo, cuyo valor se guarda en la estructura Dinero Public Overloads Function ToString( _ ByVal format As String, _ ByVal provider As System.IFormatProvider) As String _ Implements System.IFormattable.ToString If String.IsNullOrEmpty(format) = True Then format = "{0:C}" End If Return String.Format(provider, format, Me.Valor) End Function Esta función pertenece a la interfaz IConvertible, pero la pongo aquí para guardar cierta lógica de presentación Public Overloads Function ToString( _ ByVal provider As System.IFormatProvider) As String _ Implements System.IConvertible.ToString Return CType(Me.Valor, IConvertible).ToString(provider) End Function
Implements IConvertibleSi necesitamos convertir valores Dinero en otros valors (Integer,float, etc) debemos implementar la intrefaz IConvertible que define métodos que convierten el valor de la referencia o tipo de valor de implementación en un tipo de Common Language Runtime con un valor equivalente. Más información en:
Puedes ver los miembros que se implementan en la documentación Microsoft de referencia de esta interfaz. Como cosa curiosa, te muestro la implementación del método ToString que se hacen esta interfaz. Public Overloads Function ToString( _ ByVal provider As System.IFormatProvider) As String _ Implements System.IConvertible.ToString Return CType(Me.Valor, IConvertible).ToString(provider) End Function Otros métodos ToString se implementen en la interfaz IFormattable
Implements IDisposableLa interfaz define un método para liberar los recursos no administrados asignados. He implementado esta interfaz para liberar el objeto CultureInfo (y todos sus objetos hijos), cuando la estructura sea reciclada por el "Recolector de basura" Más información en:
Espacios de nombres usados en el código de este artículo:System.Windows.Forms
|
Lo comentado en este artículo está probado (y funciona) con la siguiente configuración:
El autor se compromete personalmente de que lo expuesto en este artículo es cierto y lo ha comprobado usando la configuración indicada anteriormente.
En cualquier caso, el Guille no se responsabiliza del contenido de este artículo.
Si encuentras alguna errata o fallo en algún link (enlace), por favor comunícalo usando este link:
Gracias.
Código de ejemplo (comprimido): |
Fichero con el código de ejemplo: jms32_ElPatronMoneda.zip - 265.00 KB
|