`

你真的会数钱吗?

阅读更多

摘要:
货币,记账相关的领域模型,使用值对象

快年底了,假如你们公司的美国总部给每个人发了一笔201212.21美元的特别奖金,作为程序员的你, 该如何把这笔钱收入囊中?

1.美元?美元!

你可能觉得,这根本不是问题。在自己的账户中直接加上一笔“转入”就行了。但是首先就遇到了币种的问题。

一般来说,银行账户都是单币种的。你可能会说不对啊,我的一卡通就能存入不同的币种啊?但那是一个“账号(Account Number)”对应的多个“账户(Account)”。 通常财务记账的时候,一个“账户(Account)”都使用同一币种。

账户(Account)记录了资金的往来,包含很多条目(Entry)。账户会记录结余,结余等于所有条目中金额的总和。

我们不可能为每个币种设计一种条目,所以需要抽象出一个货币类——Money,适用于各种不同的币种:

Money类

Money类至少要记录金额和币种:

  • 对于金额,由于货币存在最小面额,所以金额的类型可以采用定点小数或者整型。考虑到会对金额进行一些运算,用整数处理应该更方便。如果用java语言实现,可以使用long类型。

  • 对于币种,java提供了java.util.Currency类,专门用于表示货币,符合ISO 4217货币代码标准。Currency使用Singleton模式,需要用getInstance方法获得实例。

    主要的方法包括:

    • String getCurrencyCode() 获取货币的ISO 4217货币代码
    • int getDefaultFractionDigits() 获取与此货币一起使用的默认小数位数
    • static Currency getInstance(Locale locale) 返回给定语言环境的国家/地区的 Currency 实例
    • static Currency getInstance(String currencyCode) 返回给定货币代码的 Currency 实例。
    • String getSymbol() 获取默认语言环境的货币符号
    • String getSymbol(Locale locale) 获取指定语言环境的货币符号
    • String toString() 返回此货币的 ISO 4217 货币代码

    通过Currency类的帮助,我们的Money类看起来大概是这个样子(为了方便,提供多种构造函数):

    public class Money {
        private long amount;
        private Currency currency;
    
        public double getAmount() {
            return BigDecimal.valueOf(amount, currency.getDefaultFractionDigits()).doubleValue();
    
        }
    
        public Currency getCurrency() {
            return currency;
        }
    
        public Money(double amount, Currency currency) {
            this.currency = currency;
            this.amount = Math.round(amount * centFactor());
        }
    
        public Money(long amount, Currency currency) {
            this.currency = currency;
            this.amount = amount * centFactor();
        }
    
        private static final int[] cents = new int[] { 1, 10, 100, 1000,10000 };
    
        private int centFactor() {
            return cents[currency.getDefaultFractionDigits()];
        }
    }
    

用Money类表示我们的$201212.21奖金,就是:

Money myMoney = new Money(201212.21,Currency.getInstance(Locale.US));

2.存入账户

终于解决了币种的问题,可以把钱存入账户了。存入的逻辑是:在条目中记录一笔账目,并计算账户的余额。

不同币种之间相加或相减是没有意义的,为了避免人为错误,在Money的代码中就要禁止这种操作。我们可以采用抛出异常的方式。 为了简单起见,这里不再定义一个单独的"MoneyException",而是直接使用java.lang.Exception:

public Money add(Money money) throws Exception{
    if(!money.getCurrency().equals(this.currency)){
        throw(new Exception("different currency can't be add"));
    }
    BigDecimal value = this.getAmount().add(money.getAmount());
    Money result = new Money(value.doubleValue(),this.getCurrency());
    return result;
}

public Money minus(Money money) throws Exception{
    if(!money.getCurrency().equals(this.currency)){
        throw(new Exception("different currency can't be minus"));
    }

    BigDecimal value =this.getAmount().add(money.getAmount().negate());
    Money result = new Money(value.doubleValue(),this.getCurrency());
    return result;

}

3.收税

先不要高兴得太早,这笔钱属于“一次性所得”,需要交20%的个人所得税。税后所得应该是多少?

你可能说:是80%。只要为Money加上一个multiply(double factor)方法就可以进行计算了。

但是牵扯到了舍入的问题。由于货币存在最小单位,在做乘/除法运算的时候就要考虑到舍入的问题了。最好是能够控制舍入的行为。假如税务部门对于 舍入的计算有明确规定,我们也可以做一个遵纪守法的好公民。

在java.math.BigDecimal中定义了7种舍入模式:

  • ROUND_UP:等于远离0的数。
  • ROUND_DOWN:等于靠近0的数。
  • ROUND_CEILING:等于靠近正无穷的数。
  • ROUND_FLOOR:等于靠近负无穷的数。
  • ROUND_HALFUP:等于靠近的数,若舍入位为5,应用ROUNDUP。
  • ROUND_HALFDOWN:等于靠近的数,若舍入位为5,应用ROUNDDOWN。
  • ROUND_HALFEVEN:舍入位前一位为奇数,应用ROUNDHALFUP;舍入位前一位为偶数,应用ROUNDHALFDOWN。

我们可以借用这些模式作为参数:

public static final int ROUND_UP = BigDecimal.ROUND_UP;
public static final int ROUND_DOWN = BigDecimal.ROUND_DOWN;
public static final int ROUND_CEILING = BigDecimal.ROUND_CEILING;
public static final int ROUND_FLOOR = BigDecimal.ROUND_FLOOR;
public static final int ROUND_HALF_UP = BigDecimal.ROUND_HALF_UP;
public static final int ROUND_HALF_DOWN = BigDecimal.ROUND_HALF_DOWN;
public static final int ROUND_HALF_EVEN = BigDecimal.ROUND_HALF_EVEN;
public static final int ROUND_UNNECESSARY = BigDecimal.ROUND_UNNECESSARY;


public Money multiply(double multiplicand, int roundingMode) {
    BigDecimal amount = this.getAmount().multiply(new BigDecimal(multiplicand));
    amount = amount.divide(BigDecimal.ONE,roundingMode);
    return new Money(amount.doubleValue(),this.getCurrency());
}

public Money divide(double divisor, int roundingMode) {
    BigDecimal amount = this.getAmount().divide(new BigDecimal(divisor),
            roundingMode);
    Money result = new Money(amount.doubleValue(), this.getCurrency());
    return result;
}

4.转成人民币

尽管各领域的国际化提了十几年,但是在国内想直接用美元消费还是有一定困难。所以你决定将这笔钱换成人民币。

对于账户来说,就是在美元账户和人民币账户分别做一笔转出和转入。 转入和转出的amount值是不同的,因为涉及到币种转换的问题。 显然,账户对象不应该知道如何进行汇率转换,责任又落在了Money类上。

最直观的做法是在Money类上增加一个convertTo(Currency currency)的方法。 但汇率实在是一个复杂的问题:

  • 汇率是经常变化的;
  • 汇率转换时的舍入处理会有相关的约定;

这些复杂的问题处理如果直接放在Money类上会显得十分笨重,单独设计一个MoneyConverter类会比较好:

import java.util.Currency;

public interface MoneyConverter {
    Money convertTo(Money money,Currency currency) throws Exception;
}

我们实现一个最简单的转化器,使用固定的汇率值:

import java.math.BigDecimal;
import java.util.Currency;
import java.util.Locale;

public class SimpleMoneyConverter implements MoneyConverter {

    private static final BigDecimal DOLLAR_TO_CNY =  new BigDecimal(6.2365);
    private static final Currency DOLLAR = Currency.getInstance(Locale.US);
    private static final Currency CNY = Currency.getInstance(Locale.CHINA);
    @Override
    public Money convertTo(Money money,Currency target) throws Exception{
        if(!known(money.getCurrency()) || !known(target)){
            throw (new Exception("unknown currency"));
        }

        BigDecimal factorSource =BigDecimal.ONE, factorTarget = BigDecimal.ONE;
        if(money.getCurrency().equals(DOLLAR))
                factorSource = DOLLAR_TO_CNY;
        if(target.equals(DOLLAR))
                factorTarget = DOLLAR_TO_CNY;
        BigDecimal value = money.getAmount().multiply(factorSource).divide(factorTarget);

        return new Money(value.doubleValue(),target);
    }

    private boolean known(Currency currency){
        return(currency.equals(DOLLAR) || currency.equals(CNY) );
    }

}

可以看到,即使是最简单的转换器,处理起来也比较麻烦。所以千万不要在Money类中做这件事情。

通过转换器可以很容易得到转成人民币后的值。

5.分钱

有好处不能独享。这笔钱你决定和老婆三七开。当然,你三!

这又是一个新的舍入问题:即使你指定各自的舍入计算方法,也不能保证各部分舍入后的值加总后仍等于原值。

前面的“可定制乘除法”似乎不能很好的解决这个问题,所以我们需要一个新的方法: Money[] allocate(double[] ratioes)

传入分配比例的数组,返回分配结果的数组。

为了保证分配的公平,可以使用伪随机数来处理误差。

该方法的实现如下:

public Money[] allocate(double[] ratioes) throws Exception{
    if(ratioes.length==0){
        throw (new Exception("there is no ratio"));
    }

    double ratioTotal = 0;
    for(double ratio:ratioes){
        ratioTotal += ratio;
    }

    if(0==ratioTotal){
        throw(new Exception("total of ratioes is zero"));
    }


    double total = this.getAmount().doubleValue();
    double delta = total;
    Money[] results = new Money[ratioes.length];

    for(int i=0;i<ratioes.length;i++){
        double amount = total*ratioes[i]/ratioTotal;
        results[i] = new Money(amount,this.getCurrency());
        delta -= results[i].getAmount().doubleValue();
    }

    int i = (int)(Math.random() * ratioes.length); 
    results[i] = results[i].minus(new Money(delta,this.getCurrency()));
    return results;
}

6.记账

将一切重要的数据保存到数据库是很通常的做法。但是将Money保存到数据库的时候,你要小心了!

Money不能作为单独的实体。如果把Money当做实体来处理,就会产生一些问题:

会有很多实体关联到Money,比如本文中的Account,Entry等。 需要非常小心处理对Money对象的引用,避免多个实体引用到同一个Money对象。在第一点的前提下,这会变得很困难。 所以应该把Money嵌入到需要的实体中,而不是把Money作为单独的实体。这样,Money仅仅是实体对象(比如Entry)的一个属性,只不过其具有多个内置的属性值。

在JPA中,可以使用@Embeddable来标注Money类。

更复杂的情况是,由于一个Account中的所有Entry都应该具有相同的Currency,将Currency保存到Account中会更简洁,Entry中只记录ammount。

可以为Money的currency属性增加@Transient标注,在Entry类的getMoney中进行组装。

7.来点高级的

在DDD(领域驱动设计)中,Money是典型的值对象(Value Object)。值对象与实体的根本区别是:值对象不需要进行标识(ID)。

这会带来一些处理上的不同:

  • 实体对象根据ID判断是否相等,值对象只根据内部属性值判断是否相等
  • 值对象通常小而且简单,创建的代价较小
  • 值对象只传递值,不传递对象引用,不用判断值对象是否指向同一个物理对象
  • 通常将值对象设计为通过构造函数进行属性设置,一旦创建就无法改变其属性值
  • 由于值对象根据内部属性值判等,我们要为Money类覆盖equals方法: public boolean equals(Object other)

8.其他未尽事宜

我们还可以为Money类增加互相比较的方法(略)

可以在构造函数中进行格式校验(略)

可以增加一些帮助显式的方法 使用currency的getSymbol(Locale locale)方法、和NumberFormat的format方法,比如:

NumberFormat nf=NumberFormat.getCurrencyInstance(Locale.CHINA);

String s=nf.format(73084.803984);// result:¥73,084.80

9.小结

本文探讨如何在应用中处理货币类型,包括币种转换、各种计算、如何持久化等内容。

货币类型是典型的值对象,本文也介绍了一点值对象的特点。更多的内容可以参考DDD。

 

 

原文:http://thinkinside.tk/2013/01/01/money.html

分享到:
评论

相关推荐

    html5微信小游戏源码 数钱游戏(仅用于参考)

    html5微信小游戏源码 数钱游戏(仅用于参考)html5微信小游戏源码 数钱游戏(仅用于参考)html5微信小游戏源码 数钱游戏(仅用于参考)html5微信小游戏源码 数钱游戏(仅用于参考)html5微信小游戏源码 数钱游戏(仅...

    html5微信小游戏源码 数钱数到手抽筋(仅用于参考)

    html5微信小游戏源码 数钱数到手抽筋(仅用于参考)html5微信小游戏源码 数钱数到手抽筋(仅用于参考)html5微信小游戏源码 数钱数到手抽筋(仅用于参考)html5微信小游戏源码 数钱数到手抽筋(仅用于参考)html5...

    微信疯狂数钱

    【微信疯狂数钱】是一款基于HTML5技术的微信小游戏,玩家可以在这里体验数钱的乐趣,无需下载安装,只需在微信环境下打开即可游玩。这款游戏利用了JavaScript库CreateJS进行开发,CreateJS是一个强大的开源框架,...

    天天数钱小游戏源码

    【天天数钱小游戏源码】是一款专为安卓设备设计的小游戏源代码,它提供了一种趣味性的数钱体验。在这款游戏中,用户可以模拟数钞票的过程,享受虚拟财富积累的乐趣。源码的开放性使得开发者或者编程爱好者有机会深入...

    微信小游戏~数钱

    在开发微信小游戏“数钱”时,通常会涉及以下几个关键的技术点: 1. **微信小游戏框架**:微信小游戏使用的是腾讯自家的小游戏开发框架,它基于JavaScript和WebGL,提供了丰富的API和工具,使得开发者可以快速构建...

    微信H5 数钱游戏-数钱数到手抽筋.rar

    微信H5 数钱游戏-数钱数到手抽筋,30秒内谁的钱最多者获胜,手都快抽筋,你也来试试看!若真有那么多钱可以数,宁愿手抽筋啊,哈哈!这个游戏调用了部分远程文件,目前来说一切正常,但后期若服务器文件失效,则游戏...

    数钱数到手抽筋HTML5游戏源码

    在"sqsdscj"这个压缩包中,通常会包含以下几类文件: 1. HTML文件:游戏的主页面,包含了游戏的结构和JavaScript引用。 2. JavaScript文件:包含游戏的逻辑代码,用于处理用户交互、动画效果和游戏规则。 3. CSS...

    天天数钱安卓游戏源码

    【天天数钱安卓游戏源码】是一款用于学习和研究的Android平台游戏开发示例,它为开发者提供了深入了解安卓游戏编程的机会。在这个项目中,你可以看到一个完整的安卓游戏从设计到实现的所有过程,这对于想要提升自己...

    数钱游戏

    4. **音频处理**:游戏可能会有背景音乐和音效,开发者需要了解如何集成和控制音频资源。 5. **游戏逻辑**:"数钱游戏"的核心部分可能是金钱计算,涉及到数学逻辑,包括加减乘除、找零算法等。这部分需要编写单元...

    数钱数到手抽筋小游戏

    【数钱数到手抽筋小游戏】是一款基于jQuery JavaScript库开发的互动小游戏,旨在通过编程技术模拟数钱的场景,让玩家在娱乐中学习和理解JavaScript的基础操作和事件处理。这个游戏的设计理念是将枯燥的编程知识与...

    H5游戏源码 数钱数到手抽筋.zip

    《H5游戏源码:构建你的数字挑战》 在当今的互联网时代,H5游戏以其轻便、跨平台的特性,深受用户喜爱。H5游戏源码是开发这些游戏的基础,它是由HTML5、CSS3和JavaScript等技术编写的代码集合,能够运行在各种...

    数钱游戏源码.zip

    3. **图形用户界面(GUI)开发**:数钱游戏通常会有图形化的界面,展示游戏画面、按钮、计分板等元素。开发者需要使用GUI库或框架,如Python的Tkinter、Pygame,或者HTML5的Canvas等,来创建这些界面元素,并实现...

    Android天天数钱

    在"天天数钱"应用中,可能会有专门的布局文件设计用于展示和操作金钱数额,以及应用图标。 5. **gen**目录:在使用Android开发工具时,会自动生成R.java文件,这个文件包含了应用中所有资源的ID。开发者可以通过...

    《HTML5 Canvas学习笔记(10)》数钱数到手抽筋

    综上所述,这篇博客文章会是一篇深入实践的HTML5 Canvas教程,通过一个有趣的数钱示例,帮助读者掌握Canvas绘图和动画制作的核心技巧。对于想要提升Web前端技能,尤其是对Canvas感兴趣的开发者来说,这是一个极好的...

    数钱游戏html

    7. **性能优化**:为了保证游戏在移动设备上流畅运行,开发者可能会采用性能优化技巧,如减少不必要的重绘和回流,或者利用Web Workers进行后台计算,避免阻塞主线程。 总的来说,【数钱游戏html】展示了HTML5在...

    微信小游戏源码 数钱游戏(仅用于学习参考)

    微信小游戏源码 数钱游戏(仅用于学习参考)微信小游戏源码 数钱游戏(仅用于学习参考)微信小游戏源码 数钱游戏(仅用于学习参考)微信小游戏源码 数钱游戏(仅用于学习参考)微信小游戏源码 数钱游戏(仅用于学习...

    微信朋友圈【数钱数到手抽经】html5小游戏源码

    在“数钱数到手抽经”游戏中,`&lt;canvas&gt;`元素被用来绘制游戏场景,包括数钱的动画效果和用户操作的反馈。 接着,我们来看JavaScript,它是HTML5游戏的核心动力。JavaScript负责处理游戏逻辑、用户输入、动画帧更新...

    android 天天数钱游戏

    为了提升游戏体验,开发者可能会使用Android的Animation API来实现数钱动画,例如钱片飘落、手指滑动等效果,增强视觉吸引力。 【核心知识点七】:音效与音乐 游戏中的音效和背景音乐可以提升沉浸感。Android提供...

    shuqian.rar_数钱

    《数钱问题与剩余定理的应用》 在计算机科学领域,尤其是算法设计和数学理论的交叉部分,我们经常遇到各种有趣的挑战,其中之一便是"数钱问题"。标题中的"shuqian.rar_数钱"显然指向了一个与计算和计数货币相关的...

Global site tag (gtag.js) - Google Analytics