18.7 货币
把金额与币种组合成值对象,集中舍入、比较和算术规则。
学习目标
- 能用金额、币种和最小单位表达订单价格,并拒绝跨币种直接算术
- 能固定舍入、比较、分配和汇率边界,避免浮点漂移与隐式精度变化
- 能在零值、负值、边界金额和换汇场景中验证货币对象,并说明何时拆出新的策略
为什么 18.7 货币值得单独学习
↡把数值金额、币种和算术规则组合在一起的值对象,负责同币种计算与合法性检查。货币不是一个数字加一个字符串。100.00 USD 与 100.00 CNY
的数值相同但语义不同,不能直接相加;金额的精度、舍入、负值和序列化也必须在一个可复核的边界内决定。
订单系统基础设施替换能暴露这个取舍。价格、折扣、税额和退款都需要同一套金额语义;如果它们在不同调用处使用浮点数,得到的总额可能因为二进制表示和不同舍入时机而分叉。把金额与币种组合成值对象,可以把错误从“结果看起来差一点”提前变成明确拒绝。
↡标识金额所属法定或业务计价体系的代码,例如 USD、CNY;它是货币对象相等和算术兼容性的组成部分。币种必须参与相等和加减判断;汇率转换不是简单的类型转换,而是带有时间、来源和舍入策略的业务事件。
↡以整数表示货币最小可计量单位的数值,例如分或厘,用于避免浮点数表达金额。最小单位让加减和比较保持整数语义,但不同币种的最小单位可能不同,映射器需要保存币种精度而不是假设所有货币都两位小数。
先建立直觉:精度规则必须有人负责
↡规定除法、税额、折扣或汇率结果在目标精度不足时如何取整,并要求相同场景使用同一规则。舍入策略不能藏在数据库、语言运行时或格式化函数里。计算中间值、展示值和结算值可能有不同精度,但每个边界都要说明何时舍入以及差额由谁承担。
↡把一种币种的金额按给定时间、来源和方向转换为另一种币种的规则或结果。汇率必须带来源、有效时间和适用方向;Money 可以拒绝跨币种运算,但不应假装自己能凭一个数字完成换汇。
先预测:订单含 0.1、0.2 和一笔 10% 折扣时,用浮点数先相加再舍入,和每行先舍入再相加会不会总是相同?把两条路径的最小单位、舍入时点和差额归属写出来,再决定货币对象的算术接口。
目录单元到教学证据
18.7 货币
在 18.7 货币 的单元边界内,学习者要把订单系统基础设施替换拆成构造、同币种算术、跨币种拒绝、舍入和汇率转换五类证据。单元键为 poeaa24-pattern-47-money;核心证据是无浮点漂移、同币种操作可复核、跨币种操作必须显式换汇,并且零值、负值和边界金额都有测试。
评审记录还要回答:金额使用什么最小单位,币种精度从哪里来,Rounding Policy 由谁选择,汇率的时间和来源如何保存,数据库和 API 如何表示。若调用者可以直接取出裸数字再自由运算,货币语义已经泄漏。
专属代码案例:整数最小单位的 Money
下面的代码用整数最小单位表达金额,并在同币种加法时检查 Currency。它把跨币种计算留给显式的汇率服务,避免 Money 静默采用一个未说明的汇率。
type CurrencyCode = "USD" | "CNY";
class Money {
private constructor(
readonly minorUnits: bigint,
readonly currency: CurrencyCode,
) {}
static fromMinorUnits(minorUnits: bigint, currency: CurrencyCode): Money {
return new Money(minorUnits, currency);
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error("currency mismatch");
}
return Money.fromMinorUnits(
this.minorUnits + other.minorUnits,
this.currency,
);
}
multiply(rate: bigint, divisor: bigint): Money {
const rounded = (this.minorUnits * rate + divisor / 2n) / divisor;
return Money.fromMinorUnits(rounded, this.currency);
}
}这段代码提供四项证据:金额以 Minor Unit 保存,同币种才能相加,跨币种操作明确失败,乘法在指定除数上采用固定舍入。生产实现还需按币种配置精度、定义负数舍入、保存汇率来源与时间,并测试分配余数不能凭浮点格式化掩盖。
模式结构图
结构图把外部表示、金额对象、同币种算术和汇率服务分开,帮助评审确认 Money 不会自行猜测跨币种转换。
舍入与换汇边界
货币计算应在金额对象、舍入策略和汇率来源之间保留可追踪证据。输入金额先规范化为最小单位,只有在明确的除法、税费或换汇边界执行 Rounding Policy,最后才格式化展示。
选择与拒绝矩阵
| 评审问题 | 选择货币对象的证据 | 应拒绝当前方案的信号 |
|---|---|---|
| 表达 | 金额、币种和 Minor Unit 一起保存 | 调用者拿裸数字自由相加 |
| 算术 | 同币种操作由 Money 检查兼容性 | 跨币种直接相加或比较 |
| 精度 | Rounding Policy 的时机和归属明确 | 依赖浮点格式化或运行时默认舍入 |
| 换汇 | Exchange Rate 有来源、时间、方向和舍入证据 | 用一个无来源数字隐式转换 |
| 替代 | 已比较裸字段、Money 和专用汇率策略 | 只因数据库类型或框架默认而选择 |
常见误区
可验证练习
练习
本组练习围绕 18.7 货币,要求把最小单位、舍入策略和汇率边界写入同一张评审卡。
问题 1:选择表示。 订单服务需要计算价格、税额和退款,且要支持 USD 与 CNY。为什么不能只用 number 加 currency 字段?
问题 2:固定舍入。 税额计算得到半个最小单位,应该在哪里决定向上、向下或银行家舍入?
问题 3:处理换汇。 一个 USD 订单需要展示 CNY 预估价,结算时汇率可能变化。Money 应该直接保存两个币种吗?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- Money
将金额、币种和同币种算术规则组合在一起的值对象。
- Currency
标识金额所属计价体系、并参与相等和算术兼容性判断的代码。
- Minor Unit
以整数表示货币最小可计量单位的金额表示方式。
- Rounding Policy
规定精度不足时取整方式、时机和差额归属的规则。
- Exchange Rate
带有来源、时间、方向和舍入规则的跨币种转换依据。
本章小结
掌握 18.7 货币的标志,不是把金额格式化成两位小数,而是能在订单系统中解释“金额与币种 → 最小单位 → 同币种算术 → 舍入策略 → 显式换汇”的责任链。Money 应拒绝跨币种直接运算,Rounding Policy 应在明确边界执行,Exchange Rate 应记录来源与时间;当系统仍依赖浮点数、默认汇率或展示精度掩盖结算精度时,应拒绝当前方案。
前后导航
来源与改写范围
- Martin Fowler 作者图书页:核对全书主题、教程与模式参考结构。
- Martin Fowler 模式目录:核对 18.7 货币的公开模式名称和相关模式族。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。