logotipo

El patrón Moneda



Descripción general

Muchas 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

[TOC] Tabla de Contenidos


↑↑↑

El patrón Moneda


↑↑↑

Antecedentes

Este 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)


↑↑↑

Uso del código

Usar 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 Moneda

Los 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 estructura

Bueno, 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ásico

El 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 flotante

Para 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 moneda

Una curiosidad: He encontrado una norma ISO (ISO 4217) que define y denomina a todas las monedas del mundo

Viendo las implementaciónes 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éticas

En 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 suma

R=(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ón

R=(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ón

R=(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


↑↑↑

Restas

Para 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ícitas

Una 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 Parse

No 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 Soportados

Esta estructura implementa las siguientes interfaces


↑↑↑

Implements ICloneable

Para 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 IComparable

Define 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 IComparable

Define 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.ValorSinRedondear)
    Else
    Return String.Format(Me.CultureInfo, "{0:C}", Me.ValorSinRedondear)
    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.ValorSinRedondear.ToString(format)
        If String.IsNullOrEmpty(format) = True Then
            format = "{0:C}"
        End If
        Return String.Format(format, Me.ValorSinRedondear)
    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.ValorSinRedondear)
    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.ValorSinRedondear, IConvertible).ToString(provider)
    End Function


↑↑↑

Implements IConvertible

Si 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.ValorSinRedondear, IConvertible).ToString(provider)
    End Function

Otros métodos ToString se implementen en la interfaz IFormattable


↑↑↑

Implements IDisposable

La 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:


↑↑↑

El código

Puedes obtener el código de la estructura Dinero y la documentación correspondiente en el siguiente enlace

Descargar el código de la estructura Dinero

MD5 checksum: 09A52F29162BF060D8427B12582BF67D


↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Documento Start]
[Imprimir el Documento]
© 1.997- 2.008 - La Güeb de Joaquín
Joaquin Medina Serrano
Ésta página es española