在.NET平台上,处理货币和金融数据是一个常见需求。NMoneys库提供了一个货币值对象(Money Value Object)的实现,它支持ISO 4217标准,允许开发者以多种货币表示和操作货币量。这意味着,使用这个库,可以轻松地创建和处理不同货币的金额。
然而,NMoneys库最初的设计并没有包括货币转换功能。所有的操作都是基于相同货币的金额定义的。尽管如此,收到了很多关于添加货币转换功能的反馈。虽然最初对此有些犹豫,但最终决定倾听这些反馈,并尝试实现这一功能。
免责声明:这不是一个货币兑换服务。它只是一个允许货币量之间进行兑换操作的手段。为了使操作准确,仍然需要从可靠的第三方获取实时的金融数据。
所有代码片段(以及为了简洁性未发布的更多代码)都将包含在演示项目中(Visual Studio 2010,.NET4.0)。
演示项目的代码也可以从项目的官方网站上浏览。同样,NMoneys库和NMoneys.Exchange库的最新代码版本可以在这里和这里轻松访问。
从一开始,就决定,无论添加什么功能,它们都将被添加到另一个项目中,而不会“污染”原始项目的简单性和专注性。
经过一些思考,得出结论,Money类上的扩展方法应该成为新功能的入口API,并且一个新的库将托管它们。这种模型将允许每个项目有不同的发布周期,并允许不需要新功能的客户保持“无膨胀”。
在独立性之后,简单性是指导库设计的另一个主题。
最初没有考虑在库中进行兑换操作的原因之一是它们的复杂性。例如,对分数数量的转换操作并不简单;存在舍入、损失等各种陷阱,当涉及到金钱时,不能忽视这些。虽然没有掌握指挥这些操作的规则的知识,但决定实现它们。
由于金额被建模为System.Decimal数量,最简单的可能的工作方式被用作“安全”的默认值:使用现有的乘法和除法操作。
很清楚,最简单的默认操作可能并不适合每个人(它们甚至可能是错误的)。为了保护正确性和完整性,提供了多个可扩展点,允许那些“知道的人”做正确的事情。如果那些开明的人能够与世界分享这些扩展,那就太好了。
一旦.NET项目引用了NMoneys.dll和NMoneys.Exchange.dll程序集,就可以通过使用NMoneys.Exchange命名空间将转换操作引入代码。
最简单的(也可能是最无用的)转换是直接转换,这意味着一个货币量,比如3欧元,如果使用所有默认值,将被转换为3美元或3英镑。显然,这是一个相当混乱的情况。
[TestMethod]
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));
}
默认值必须改变。一种方式是在方法调用链中的某个地方提供十进制汇率转换。简单、在许多层面上丑陋,但肯定可行,考虑到框架的多个扩展点。传递硬编码的汇率(它们是易变的)不是正确的方式;迫使开发者创建某种值提供者,那么为什么不将其嵌入到框架中呢?
使用直接的提供者模型来覆盖默认汇率。需要配置一个实现IExchangeRateProvider的实现,以允许更正确的转换。这是通过将委托设置到ExchangeRateProvider.Factory属性来完成的。完全“从头开始”的实现,它咨询在线提供者是受欢迎的,已经标记为无用的ExchangeRateProvider.Default也是如此。
[TestMethod]
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委托的技术。
[TestMethod]
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的外观。提到,提供一个固定数字作为汇率可能不是最聪明的想法,但人们仍然可以做到这一点。
扩展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将自己画进一个丑陋的角落。
[TestMethod]
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如何是可扩展的和非侵入性的,以便不膨胀原始项目。