量
Models are not right or wrong, they are more or less useful.
以数量及其单位来表示带有单位的量
许多情况下我们希望用计算机表示带单位的量:如6英尺或30公斤,通常只是数字,这主要是因为在特定语言只能提供有限功能的系统下我们只能那么做 。
但是使用对象让我们可以在为量增加值的时候都相应的带上了新的基本类型;而且带单位的量有一个特定的类型将很有价值。当这种带单位的量是货币时尤其是这样,货币是一种特殊的计量案例,同时它被广泛应用 — 许多人愿意花钱去注意钱。
原理
量的基本思想是一个结合了数量和单位的类,总之量的表现很简单,但有趣的是量所带来的行为,你可以用它作一些应用。
量的第一种行为是算术运算,你可以象将两个数字相加一样简单的相加两个量,进一步的,量必须至少有防止将6英尺和180磅相加减的智能。
当你要将两种相近的量相加时,更加复杂的问题随着产生,比如要将500米和3公里相加时,简单的方法是禁止相加,强制客户先进行转换。一个更复杂的方法是看你是否能将一种参数转化为其它参数,这样你就可以进行相加了,你所做的好坏程度就取决于你的Convertor转换类的复杂程度。
乘法在难度上也有类似的变化,最简单的方法是只允许以梯度量乘除,一个更复杂的方法是允许让其它量除,但你必须处理运算结果的单位,就是说你如果将150英里除以2小时得到的将是75英里/小时。
比较运算也同样需要,这样你才能比较6英尺比5英尺多,在这里的问题与相加类似-你必须选择如何转化。
通过提供一个简单的接口来允许量的直接转换非常有用,尽管转换工作通常通过Convertor更加好,如果你只有几个单位,将他们嵌入到量中的类将比较简便。
还可以给量一个有用的行为就是提供显示及解析功能这样就能让你容易的输出出字串还能从一个字串转化为量,这样一个简单的模式在文件或GUI界面方面能简化很多输入输出的工作。
对于简单的显示你可以有一个默认的方式如先显示数量然后是单位,如果你希望显示单位后是数量还是其它取决于单位,那么就可以将这种显示和解析行为委派给单位。
货币
我几乎在所有工作系统中使用量,但我很少表达物理单位,大多数时间用它来表示货币,许多物理上的单位在本质上是相同的,但是如果是钱的话你的印象会深一些。
最大的变化围绕于转化之处,虽然物理单位的转换不会随时间变化,但汇率一直在变,显然这会影响到转化操作,同样也会影响加,减及比较。
当你转换货币时,你至少需要提供一些时间参考,其粒度由应用来决定。但是许多情况下在不同的上下文环境下,你可能有独立的转换,这一点我在Convertor 有详细说明。
所有这些的结果是你需要在货币算术和比较运算中的自动转换要小心,你经常会发现那不被允许,然而对此有一个巧妙的解决方法,在Money Bag中可以找到。
另外,货币类对于量一个特别有用的功能是对舍入的处理,货币通常有小数位,但你不能用实际的数字来处理货币。原因在于实际的数字的进位几乎从不与实际需要对应,如果忽视这一点事实的话将导致间歇的BUG,虽然范围很小但非常容易导致失败。
而货币对象能封装进位规则,这意味着在绝大多数时间时你同钱打交道时不必意识到进位规则。
与此相关的时麻烦的除法,如果你将$100 除以3,将得到什么?通常结果不是简单的$33.33,问题时如果你将它乘3将得到$99.99 -意味者少了一个美分,而会计是不允许的,所以你必须找出在这种情况下的策略。经常的,这种规则是某人将得到额外的一个美分,虽然也不要紧,这样你可以在货币对象中放入一个方法返回一个由除法产生的所要分配的货币集合。
关系数据库
一个通常问题是如何在关系数据库及其它无法添加轻量级类型的系统中使用量,你为每一个货币值保存数目和货币代码吗?
在某些特定条件下要求所有的货币变成为同一种通货,一个问题产生了,考虑一下你在一个帐户下有很多科目,每种科目有一种货币属性来显示科目的金额,然而在一个帐户下所有的科目有相同的通货,只保存帐户通货,但是不保存科目之间流转,这样合理吗?
我先不谈这个问题,让它成为你数据库设计的细节,我仍然力劝你在你的代码中使用货币对象:至于你是否在数据库总是否保存就看你了,毕竟数据库完全不能象代码那样给你带来好处。
何时使用
正如你了解的我如何使用量,至少在货币的变化中 ,实际上在一个面对对象的环境下,没有什么理由不使用它,但是据我所知人们由于不熟悉而怕使用,执行效率也是一个经常使用的理由—尽管我还从没发现量确实会成为一个问题。
有一种争论说使用量并不值得,因为它只有一个单位,所以如果你只使用一种通货的话你不必使用货币这个类,但是很多的货币的价值来源于它的行为,而不是多单位的能力,所以即使如此,我仍然使用它。
例: Money
货币是我最常使用量的地方,所以能起很好的示范作用。首先是基本的表现。
class Money...
public class Money implements Comparable{
private BigInteger amount;
private Currency currency;
注意我使用了BigInteger,在JAVA中我相应的会使用BigDecimal,但是在很多语言中整数是唯一合适的选择,所以使用整数解释起来比较好,不要担心选择量的数目的表现的性能因素 ,对象的妙处在于你可以在其内部选择任何数据结构,而在外部可以隐藏这一点。
So moving to the interface. You'll probably want the obvious getting methods.
然后是接口,显而易见,你会想GET方法。
class Money...
public double amount() {
return amount.doubleValue() / 100;
}
public Currency currency() {
return currency;
}
尽管你发现自己并不经常使用它,通常其它方法会更好的达到你的目标,实际上使用getter方法会经常启发你是否还有更好的方法—一般是这样。
你会注意到没有setter方法,货币是一种Value Object并且是不可改变的。
以下有很多构造函数来帮你建立货币对象,其中从基本的数字类型进行转化通常是很有用的。
class Money...
public Money (double amount, Currency currency) {
this.amount = BigInteger.valueOf(Math.round (amount * 100));
this.currency = currency;
}
public Money (long amount, Currency currency) {
this.amount = BigInteger.valueOf(amount * 100);
this.currency = currency;
}
如果你大量使用一种通货你可能需要一种特殊的构造函数。
class Money...
public static Money dollars(double amount) {
return new Money (amount, Currency.USD);
}
对于加减法,我不做任何别巨匠心的转换,请注意我使用一个带有标志的特殊构造函数。
class Money...
public Money add (Money arg) {
assertSameCurrencyAs(arg);
return new Money (amount.add(arg.amount), currency, true);
}
public Money subtract (Money arg) {
return this.add(arg.negate());
}
void assertSameCurrencyAs(Money arg) {
Assert.equals("money math mismatch", currency, arg.currency);
}
private Money (BigInteger amountInPennies, Currency currency, boolean privacyMarker) {
Assert.notNull(amountInPennies);
Assert.notNull(currency);
this.amount = amountInPennies;
this.currency = currency;
}
public Money negate() {
return new Money (amount.negate(), currency, true);
}
乘法则非常直接。
class Money...
public Money multiply (double arg) {
return new Money (amount() * arg, currency);
}
但是除法不然,因为我们必须考虑到不定的小数,我们可以通过返回一个货币数组,这样数组的总和等于原先的数值,就是说原先的值会合理的分配到数组元素中去,
class Money...
public Money[] divide(int denominator) {
BigInteger bigDenominator = BigInteger.valueOf(denominator);
Money[] result = new Money[denominator];
BigInteger simpleResult = amount.divide(bigDenominator);
for (int i = 0; i < denominator ; i++) {
result[i] = new Money(simpleResult, currency, true);
}
int remainder = amount.subtract(simpleResult.multiply(bigDenominator)).intValue();
for (int i=0; i < remainder; i++) {
result[i] = result[i].add(new Money(BigInteger.valueOf(1), currency, true));
}
return result;
}
下面我们看看比较货币,在JAVA中的方式是实现comparable接口。
class Money...
public int compareTo (Object arg) {
Money moneyArg = (Money) arg;
assertSameCurrencyAs(moneyArg);
return amount.compareTo(moneyArg.amount);
}
如果给方法起一个较好的名字也非常有效,如:
class Money...
public boolean greaterThan(Money arg) {
return (this.compareTo(arg) == 1);
}
public boolean lessThan(Money arg) {
return (this.compareTo(arg) == -1);
}
这使比较的方法更容易理解。
既然货币是一个值,它必须重写equals方法:
class Money...
public boolean equals(Object arg) {
if (!(arg instanceof Money)) return false;
Money other = (Money) arg;
return (currency.equals(other.currency) && (amount.equals(other.amount)));
}
既然重写了equals方法,不要忘记也重写hash 方法(这有一个简单的例子)
class Money...
public int hashCode() {
return amount.hashCode();
}
注:本文只是分析模式中的一小部分,建议读者同时看一下Windy J翻译的《前言和笔记》,这样对分析模式有一个大体的了解(http://www.umlchina.com/xprogrammer/issue/1/analsispattern.htm)