65.9K
CodeProject 正在变化。 阅读更多。
Home

NMoneys.Exchange,NMoneys 的伴侣

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (4投票s)

2011 年 9 月 28 日

CPOL

6分钟阅读

viewsIcon

17276

downloadIcon

165

NMoneys.Exchange 扩展了 NMoneys(Money Value Object 实现),以支持货币兑换

NMoneys_long.png

背景 

我已经在这篇文章中写过了关于 NMoneys 的内容,但为了赶时间的人,我会做一个简短的回顾。 

NMoneys 是一个适用于 .NET 平台的“Money Value Object”实现,支持 ISO 4217 标准

这个定义意味着,有了这个库,我们就可以用各种货币来表示和操作货币数量。

项目的非正式范围规定 NMoneys 不支持不同货币之间的货币数量兑换。事实上,所有操作都定义为同一种货币的数量之间的运算。

令人惊讶的是(也许并不奇怪),我收到的反馈大多是关于添加这些功能的。起初我并不情愿,但我不得不听取意见,耸耸肩,试一试。这就是我带来的结果。

免责声明

不要产生误解:它不是一个货币兑换服务。它只是一个允许货币数量之间发生兑换操作的工具。为了使操作准确,仍然需要从可靠的第三方获取真实、当前的金融数据。

Using the Code

所有代码片段(以及因篇幅原因未发布的更多内容)都将包含在演示项目中(Visual Studio 2010, .NET 4.0)。

演示项目的代码也可以直接从项目网站查看。同样,可以轻松地从这里这里访问 `NMoneys` 和 `NMoneys.Exchange` 库的最新代码。

扩展与征服

独立性

我从一开始就确定了一件事:无论添加什么功能,都将添加到另一个项目中,而不会“污染”原始项目的简洁性和焦点。

经过一番思考,我得出结论,`Money` 类的扩展方法应该是新功能的入口 API,并且一个新的库将托管它们。这种模型允许每个项目有不同的发布周期,并允许不需要新功能的客户端保持“无臃肿”。

简单

在独立之后,简洁性是指导库设计的原则。

最初没有考虑兑换操作是该库的原因之一是它们的复杂性。诸如分数数量的转换之类的操作并非易事;存在舍入、损失以及各种陷阱,在涉及金钱时,这些都是无法避免的。我并不具备指挥这些操作的规则知识,但我却决定要实现它们。好大的胆子!好吧,也算不上。由于金额被建模为 `System.Decimal` 数量,最简单的可能起作用的方式被用作“安全”的默认值:使用现有的乘法和除法运算。

可扩展性

我非常清楚,最简单的默认操作可能不适合所有人(甚至可能是不正确的)。为了保护正确性和完整性,提供了多个可扩展点,允许“知情者”做正确的事情。如果那些开明的人能够将这些扩展贡献给世界,那将同样棒极了。;-p

转换

一旦 .NET 项目引用了 *NMoneys.dll* 和 *NMoneys.Exchange.dll* 程序集,就可以通过简单地使用 `NMoneys.Exchange` 命名空间来将转换操作引入代码。该操作将启用 `Money` 实例上的 `.Convert()` 扩展方法。

无意义的默认值

最简单(也可能最无用)的转换是直接转换,这意味着一个货币数量,例如 3 欧元,如果使用所有默认值,将被转换为 3 美元或 3 英镑。这显然是一团糟。

[Test]
public void Default_Conversions_DoNotBlowUpButAreNotTerriblyUseful()
{
    var tenEuro = new Money(10m, CurrencyIsoCode.EUR);

    var tenDollars = tenEuro.Convert().To(CurrencyIsoCode.USD);
    Assert.That(tenDollars.Amount, Is.EqualTo(10m));
            
    var tenPounds = tenEuro.Convert().To(Currency.Gbp);
    Assert.That(tenPounds.Amount, Is.EqualTo(10m));
}

默认值必须更改。一种方法是在方法调用链的某个地方提供一个十进制汇率转换。这种方式很简单,在很多层面上都很丑陋,但鉴于框架的多个扩展点,当然是可行的。传递一个硬编码的汇率(它们很不稳定)不是好办法;强迫开发人员创建某种值提供者,那么为什么不将其嵌入框架呢?

X 将提供

使用一个直接的提供者模型来覆盖默认汇率。需要配置一个实现了 `IExchangeRateProvider` 的实现,它允许更正确的转换。这可以通过将一个委托设置到 `ExchangeRateProvider.Factory` 属性来完成。一个完全“从头开始”的实现,它查询在线提供者,以及那个已经被标记为无用的 `ExchangeRateProvider.Default`,都是受欢迎的。

[Test]
public void Configuring_Provider()
{
	var customProvider = new TabulatedExchangeRateProvider();
	customProvider.Add(CurrencyIsoCode.EUR, CurrencyIsoCode.USD, 0);

	ExchangeRateProvider.Factory = () => customProvider;

	var tenEuro = new Money(10m, CurrencyIsoCode.EUR);
	var zeroDollars = tenEuro.Convert().To(CurrencyIsoCode.USD);

	// go back to default
	ExchangeRateProvider.Factory = ExchangeRateProvider.Default;
}

从示例中,你可以看到 `IExchangeRateProvider` 的另一个实现,即 `TabulatedExchangeRateProvider`。这个提供者简化了“静态”汇率表的创建,这在某些领域可能很有用,尤其是在缓存调用方面。使用你喜欢的控制反转容器、复杂的临时类型创建策略,可以做一些非常聪明的事情来节省对实时提供者的请求。该类更全面的功能在其单元测试中得到了展示,例如添加汇率和计算其反向汇率的能力。

高度评价

无用的默认实现被抛弃,可以通过自定义提供者将新的汇率输入系统,但 `ExchangeRate` 的默认计算仍然不适合你的目的。你应该放弃吗?绝对不。能够使用自定义提供者,使得该自定义提供者能够返回 `ExchangeRate` 的继承者,这些继承者使用自定义逻辑来执行计算。

  1. 首先,想出一个执行所需运算的 `ExchangeRate` 继承者
    public class CustomRateArithmetic : ExchangeRate
    {
        public CustomRateArithmetic(CurrencyIsoCode from, 
    	CurrencyIsoCode to, decimal rate) : base(from, to, rate) { }
    
        public override Money Apply(Money from)
        {
            // instead of this useless "return 0" policy one can 
            // implement rounding policies, for instance
            return new Money(0m, To);
        }
    }
  2. 然后创建利用此自定义汇率应用逻辑的 `IExchangeRateProvider` 实现
    public class CustomArithmeticProvider : IExchangeRateProvider
    {
        public ExchangeRate Get(CurrencyIsoCode from, CurrencyIsoCode to)
        {
            return new CustomRateArithmetic(from, to, 1m);
        }
    
        public bool TryGet(CurrencyIsoCode from, 
    		CurrencyIsoCode to, out ExchangeRate rate)
        {
            rate = new CustomRateArithmetic(from, to, 1m);
            return true;
        }
    }
  3. 最后,也是最重要的,让框架意识到你的计算是由刚刚实现的提供者执行的,使用前面展示的技术:设置 `ExchangeRateProvider.Factory` 委托。
    [Test]
    public void Use_CustomArithmeticProvider()
    {
        var customProvider = new CustomArithmeticProvider();
        ExchangeRateProvider.Factory = () => customProvider;
    
        var zeroDollars = 10m.Eur().Convert().To(CurrencyIsoCode.USD);
        Assert.That(zeroDollars, Is.EqualTo(0m.Usd()));
        // go back to default
        ExchangeRateProvider.Factory = ExchangeRateProvider.Default;
    }

重新定义 API

你可以走得很远,重新定义 API 的外观。我们提到提供一个固定数字作为汇率可能不是最聪明的想法,但你仍然可以做到。

  1. 扩展 `IExchangeConversion` 入口点以返回自定义类型。
    public static UsingImplementor Using
    	(this IExchangeConversion conversion, decimal rate)
    {
        return new UsingImplementor(conversion.From, rate);
    }
  2. 实现自定义类型。
    public class UsingImplementor
    {
        private readonly Money _from;
        private readonly decimal _rate;
        public UsingImplementor(Money from, decimal rate)
        {
            _from = from;
            _rate = rate;
        }
        public Money To(CurrencyIsoCode to)
        {
            var rateCalculator = new ExchangeRate(_from.CurrencyCode, to, _rate);
            return rateCalculator.Apply(_from);    
        }
    }
  3. 使用你新塑造的 API 来把自己弄进一个糟糕的境地。;-)
    [Test]
    public void Creating_New_ConversionOperations()
    {
        var hundredDollars = new Money(100m, CurrencyIsoCode.USD);
    
        var twoHundredEuros = hundredDollars.Convert().Using(2m).To
    					(CurrencyIsoCode.EUR);
    
        Assert.That(twoHundredEuros, Is.EqualTo(200m.Eur()));
    }

当然,API 的可扩展性可以用来解决一些其他问题,例如为买入/卖出货币提供不同的转换,或者许多我无法虚构的其他智能场景。

总结

我写这篇文章有几个目标。

表明 NMoneys 库仍然活跃且健康。

其次,反馈非常受欢迎。因此,NMoneys.Exchange 被实现出来了。

最后但同样重要的是,借此机会展示一个 API 如何可以具有可扩展性且非侵入性,从而避免臃肿原始项目。

编写代码,分享它,并快乐地生活。

历史

  • 2011 年 9 月 25 日 - 初始版本
© . All rights reserved.