18.7 货币

把金额与币种组合成值对象,集中舍入、比较和算术规则。

学习目标

  • 能用金额、币种和最小单位表达订单价格,并拒绝跨币种直接算术
  • 能固定舍入、比较、分配和汇率边界,避免浮点漂移与隐式精度变化
  • 能在零值、负值、边界金额和换汇场景中验证货币对象,并说明何时拆出新的策略

为什么 18.7 货币值得单独学习

货币不是一个数字加一个字符串。100.00 USD100.00 CNY 的数值相同但语义不同,不能直接相加;金额的精度、舍入、负值和序列化也必须在一个可复核的边界内决定。

订单系统基础设施替换能暴露这个取舍。价格、折扣、税额和退款都需要同一套金额语义;如果它们在不同调用处使用浮点数,得到的总额可能因为二进制表示和不同舍入时机而分叉。把金额与币种组合成值对象,可以把错误从“结果看起来差一点”提前变成明确拒绝。

币种必须参与相等和加减判断;汇率转换不是简单的类型转换,而是带有时间、来源和舍入策略的业务事件。

最小单位让加减和比较保持整数语义,但不同币种的最小单位可能不同,映射器需要保存币种精度而不是假设所有货币都两位小数。

先建立直觉:精度规则必须有人负责

舍入策略不能藏在数据库、语言运行时或格式化函数里。计算中间值、展示值和结算值可能有不同精度,但每个边界都要说明何时舍入以及差额由谁承担。

汇率必须带来源、有效时间和适用方向;Money 可以拒绝跨币种运算,但不应假装自己能凭一个数字完成换汇。

先预测:订单含 0.10.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 保存,同币种才能相加,跨币种操作明确失败,乘法在指定除数上采用固定舍入。生产实现还需按币种配置精度、定义负数舍入、保存汇率来源与时间,并测试分配余数不能凭浮点格式化掩盖。

模式结构图

18.7 货币:金额、币种与算术边界价格 / 税 / 退款外部输入规范化MoneyMinor Unit + Currency加减 / 比较跨币种 → 拒绝结算边界Rounding Policy结算金额Exchange Rate来源 / 时间显式换汇Money 不猜汇率;舍入与换汇都必须留下可重放证据把货币错误变成边界上的明确拒绝
货币对象集中最小单位和同币种算术,舍入与换汇分别由明确策略和服务承担。

结构图把外部表示、金额对象、同币种算术和汇率服务分开,帮助评审确认 Money 不会自行猜测跨币种转换。

舍入与换汇边界

18.7 货币:计算、舍入与换汇顺序Minor Unit整数金额币种精度计算税 / 折扣 / 除法保留中间值Rounding Policy结算边界模式 + 归属差额可追踪换汇 / 展示汇率来源最终格式化内部精度、结算精度和展示精度分别声明舍入发生在业务边界,不发生在最后一行格式化
最小单位承载精确计算,舍入策略决定结算差额,换汇和展示各有独立证据。

货币计算应在金额对象、舍入策略和汇率来源之间保留可追踪证据。输入金额先规范化为最小单位,只有在明确的除法、税费或换汇边界执行 Rounding Policy,最后才格式化展示。

选择与拒绝矩阵

评审问题选择货币对象的证据应拒绝当前方案的信号
表达金额、币种和 Minor Unit 一起保存调用者拿裸数字自由相加
算术同币种操作由 Money 检查兼容性跨币种直接相加或比较
精度Rounding Policy 的时机和归属明确依赖浮点格式化或运行时默认舍入
换汇Exchange Rate 有来源、时间、方向和舍入证据用一个无来源数字隐式转换
替代已比较裸字段、Money 和专用汇率策略只因数据库类型或框架默认而选择

常见误区

可验证练习

练习

本组练习围绕 18.7 货币,要求把最小单位、舍入策略和汇率边界写入同一张评审卡。

问题 1:选择表示。 订单服务需要计算价格、税额和退款,且要支持 USD 与 CNY。为什么不能只用 numbercurrency 字段?

问题 2:固定舍入。 税额计算得到半个最小单位,应该在哪里决定向上、向下或银行家舍入?

问题 3:处理换汇。 一个 USD 订单需要展示 CNY 预估价,结算时汇率可能变化。Money 应该直接保存两个币种吗?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

Money

将金额、币种和同币种算术规则组合在一起的值对象。

Currency

标识金额所属计价体系、并参与相等和算术兼容性判断的代码。

Minor Unit

以整数表示货币最小可计量单位的金额表示方式。

Rounding Policy

规定精度不足时取整方式、时机和差额归属的规则。

Exchange Rate

带有来源、时间、方向和舍入规则的跨币种转换依据。

本章小结

掌握 18.7 货币的标志,不是把金额格式化成两位小数,而是能在订单系统中解释“金额与币种 → 最小单位 → 同币种算术 → 舍入策略 → 显式换汇”的责任链。Money 应拒绝跨币种直接运算,Rounding Policy 应在明确边界执行,Exchange Rate 应记录来源与时间;当系统仍依赖浮点数、默认汇率或展示精度掩盖结算精度时,应拒绝当前方案。

前后导航

来源与改写范围

资料与写作方式声明

本章以Martin Fowler《企业应用架构模式》与公开模式目录权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…