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

背景
我已经在这篇文章中写过了关于 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` 的继承者,这些继承者使用自定义逻辑来执行计算。
- 首先,想出一个执行所需运算的 `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); } }
- 然后创建利用此自定义汇率应用逻辑的 `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; } }
- 最后,也是最重要的,让框架意识到你的计算是由刚刚实现的提供者执行的,使用前面展示的技术:设置 `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 的外观。我们提到提供一个固定数字作为汇率可能不是最聪明的想法,但你仍然可以做到。
- 扩展 `IExchangeConversion` 入口点以返回自定义类型。
public static UsingImplementor Using (this IExchangeConversion conversion, decimal rate) { return new UsingImplementor(conversion.From, rate); }
- 实现自定义类型。
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); } }
- 使用你新塑造的 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 日 - 初始版本