freeCodeCamp.org

What to Do When Your Constants Are Someone Else's Law

8.5内容质量

TL;DR · AI 摘要

处理外部法规常量时需警惕三类错误:误用数据源、未跟踪变更、验证逻辑错位,需建立专门规则管理机制。

核心要点

  • GST利息计算错误6倍源于误用总负债而非实际现金支付数据
  • 法规常量变更可能引发合规风险且无预警机制
  • 应建立独立规则存储层实现变更追踪与验证隔离

结构提纲

按章节快速跳转。

  1. 通过GST利息计算错误案例揭示法规常量处理的特殊性

  2. 展示因错误选择计算基数导致的6倍利息差异案例

  3. 说明法规常量可能在未通知情况下变更的法律风险

  4. 分析表单验证与业务逻辑分离导致的合规漏洞

  5. 提出规则存储、变更追踪、验证隔离的应对策略

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • 法规常量处理陷阱
    • 数据源误用
      • 总负债vs现金支付差异
    • 变更风险
      • 无预警机制
    • 验证错位
      • 表单验证与业务逻辑分离

金句 / Highlights

值得收藏与分享的关键句。

#软件工程#法规遵从#JavaScript#Go
打开原文

当你的常量成为他人的法则时该怎么做

2026年10月11日

/

#软件工程

Javeed Shaik

一位同事曾问我,为什么代码中的一个数字有长达三行的注释。这个数字是18。

注释说明了这个数字来自哪条规则、哪个条款通知了它,并指出它可能在不提前警告的情况下发生变化。这让我觉得对一个两位数的整数来说,这种仪式感有些过度——直到我记起了另一种可能性。

在大多数软件中,常量是你自己做出的决定。而在实现法规的软件中,常量是别人做出的决定,他们可以在不通知你的情况下更改它,而它的合法正确值可能一开始你就没有正确读取。

我在构建一套金融计算器时,通过艰难的方式学到了这两方面的经验。以下是发生的问题,以及我现在对任何编码实现他人拥有的规则的代码所应用的三个习惯。

本文面向那些编写实现他人拥有的规则的代码的开发者,例如税务、薪资、福利、借贷,或任何由监管机构或标准机构设定数值并可随时修订的领域。如果你曾经硬编码过后来发生变化的税率,那么本文就是为你准备的。

先决条件:

你只需要掌握足够的JavaScript和Go语言来阅读短代码片段。以下内容中会用到这两种语言,但仅作为示例。此处的内容并不依赖你对这两种语言有深入的了解。

我不假设你有任何财务或税务背景,并会在每个术语首次出现时进行解释。这些示例来自印度的GST,因为这是我遇到这些错误的地方,但这三个失败模式并不特定于印度、税务或任何特定司法管辖区。

我们将涵盖的内容:

  • 返回看似合理数值的错误
  • 睡觉时悄悄改变的常量
  • 生活在表单中的验证
  • 三个习惯
  • 总结

返回看似合理数值的错误

让我们从最糟糕的一种开始,因为它看起来根本不像错误。

在印度,如果你延迟支付商品和服务税(GST),就需要支付利息。显然的实现方式是:取应缴税款,乘以年利率,再乘以延迟天数的比例。

code
const interest = taxOwed * (ratePct / 100) * (days / 365);

这段代码在数学上是完美的。然而,在大量实际案例中,它计算出的利息值却是实际值的数倍。

原因在于“应缴税款”不是一个单一数值。GST申报表包含毛输出负债:你销售所有商品的总税额。但大多数企业会通过进项税抵扣(即他们自己购买商品时已支付的税款)抵消大部分税款。只有剩余部分需要以现金支付。而根据相关规则,利息是基于实际现金支付的税款计算,而非基于总负债。

因此我们使用了相同的公式、相同的税率,但输入却存在差异。其中一个输入是错误的:

code
延迟90天,年利率18%
  按实际现金支付税款(₹50,000)→ ₹2,219
  按总负债(₹3,00,000)→ ₹13,315

利息是实际值的六倍。这两个数值都是公式返回的结果。没有抛出异常。它们看起来都不明显荒谬。如果你是开发者,你的测试会通过,因为测试用例与代码一样基于相同的误解编写。

这种失败模式正是使监管代码与众不同的地方。普通错误会产生堆栈跟踪,或明显错误到让人立即注意到的数值。而这种错误产生的数值形状正确、数量级合理,但却是坚定而错误的。

实际的修复方法

更好的测试无法解决这个问题。更清晰的文档注释才能。

修复方法是在定义处明确说明参数所代表的含义及其依据:

code
// GSTInterest 计算逾期缴纳 GST 产生的利息。
//
// 根据第 88B(1) 条规定,利息应基于电子现金账簿中实际扣除的税款计算 —— 而不是基于总输出应税额。如果理解错误,会导致利息被高估数倍。请将 cashTaxPaid 作为现金账簿部分传入,而非总账单。
//
// 除非通知税率已变更,否则 annualRatePct 应使用 DefaultGSTInterestRate;目前根据第 50(1) 条规定,通知税率为 18% 年利率。
func GSTInterest(cashTaxPaid float64, days int, annualRatePct float64) float64

注意参数命名为 cashTaxPaid 而非 amount。这个命名本身就是完整的防御。当调用者手头有总账单时,必须停下来思考这个数值是否正确(这正是防止错误的关键停顿)。

我甚至认为:如果参数可能与类似数值混淆,名称应彻底消除这种混淆。amount 会引发错误,cashTaxPaid 则杜绝了这种可能。

睡觉时悄悄改变的常量

第二种失败模式更慢但更尴尬。

税率档次会随年度预算调整,法定上限通过通知进行修订。抚恤金上限可能多年保持不变,然后突然取消。这些变更完全不关心你的发布计划。

诱人的实现方式是使用常量:

code
const gratuityCeiling = 2000000

现在想想这个做法伤害了谁。不仅是阅读过时数值的用户,还有周二发现变更的开发者——发现该值已硬编码到依赖项中,只能无奈地提交问题并等待。

还有一个更微妙的问题。法定数值往往不是单一值。对大多数人是某个值,但特定政府雇员的上限更高。固定期限员工的最低服务年限要求更短。隐藏的常量不仅可能过时,还会永远错误地影响整个用户群体。

因此:法定值应作为带默认值的选项公开,而非隐藏的常量。

code
// GratuityOptions 包含通过通知变更的法定数值。零值表示"使用默认值"。
type GratuityOptions struct {
    Ceiling  float64 // 0 -> 20,00,000。中央政府文职人员使用 25,00,000。
    MinYears float64 // 0 -> 5。固定期限员工使用 1。
}

零值仍能处理常见情况,因此没有人需要为复杂性支付代价。但当数值变更时,调用者可以立即自行修正——无需等待我发布新版本。

最后一句话才是关键。当你的代码封装了你无法控制的规则时,用户需要一种无需依赖你发布节奏的覆盖方式。

埋藏在表单中的验证

第三个问题在重构时发现了我,但这是最可移植的教训。

我曾有一个构建贷款摊销表的函数:每月一行,逐步遍历整个期限。在应用内时是安全的。表单有 max 属性限制,UI 会限制输入。没人能申请 200 年贷款,因为界面根本不允许。

当我将计算逻辑提取到库中时,表单就消失了。

code
rows, _ := Amortisation(principal, rate, months) // months 究竟来自哪里?

循环的边界仅取决于它被赋予的任期。如果传入了荒谬的值,它不会返回错误答案或运行缓慢,而是会一直分配内存直到调用者的进程终止。我的错误,他们的内存溢出崩溃,在我从未见过的程序中发生。

修复方法并不显眼:在函数中添加防护措施。

code
const MaxMonths = 1200 // 100 years

rows, err := Amortisation(principal, rate, 2000)
// err: months must be at most 1200, got 2000

上限被故意设置为荒谬的数值。实际上最长的住房贷款最多只到30至40年,因此100年这个数值只会对那些本就毫无意义的输入产生作用。这就是这种防护机制应具备的特性:它应该完全不可能被意外触发,因此永远不会成为他人争论的限制条件。

这个教训的普遍性远超单个函数。当你从应用程序中提取出库时,你会继承UI静默执行的所有输入验证,并且这些验证会以他人崩溃的形式继承过来。

三种习惯

当你编写实现他人制定规则的代码时,应养成以下三种习惯:

1. 在文档注释中明确标注规则来源

不要只写"the rate"。要说明是哪条规则、哪个条款,并且要注明规则可能变更。注释中明确标注Rule 88B(1)和Section 50(1)的目的是让检查我的工作的人知道该核对什么,也让有异议的读者可以去查阅原始资料而非盲目信任我。

2. 将法定数值设为带默认值的选项而非常量

零值处理常见情况。覆盖值用于法律变更时,以及默认值从未适用的用户群体。

3. 将防护机制从表单移至函数

UI执行的任何验证都是你的库未执行的验证。假设下一个调用者可能根本没有UI。

这些做法并不新颖。这是在不信任输入的常规纪律,应用于输入定义由他人设定且按其时间表修订的领域。

单独命名这些做法的价值在于该领域的特定失败特征:没有异常、没有堆栈跟踪、也没有明显错误的输出。只是某个数值形状正确但大了六倍,安静地出现在即将被处理的报告中。

总结

归根结底,这就是问题的本质。

实现法规的代码有其特有的失败特征:没有异常或堆栈跟踪,只有一个看起来正确但确定错误的数字。

我们从三个方面看到了这种现象:一个参数静默地表示了与规则意图不同的数量,一个法定数值被冻结为常量而法律后来发生了变化,以及验证逻辑在提取到库时被留在了UI中。

三种习惯依次对应解决这三个失败:在定义处标注规则来源,让下一位读者知道该核对什么;将法定数值暴露为带默认值的选项,这样当法律变更时只需修改参数而非搜索字面值;将防护机制放入函数中,因为下一位调用者可能根本没有UI。

如果必须带走一个要点,请记住命名的重要性。amount会引发错误,cashTaxPaid则杜绝错误。参数名称是你写过的最廉价的文档。

我开发了EMICalcs,这是一套面向印度的免费金融计算器,涵盖贷款、定期定额投资(SIP)、商品及服务税(GST)、退休金和税务计算。上述公式已从该工具中提取,并作为开源库发布,因此任何人都可以审计这些计算公式以验证我的工作。

合作伙伴推荐的课程

(在新标签页中打开)

广告

  • Coursera上的GST供应时间解释与案例分析 EDUCBA 3周,每周2小时 中级证书 查看课程 (在新标签页中打开)
  • Coursera上的GST合规与申报 Tally Education and Distribution Services Private Limited 5周,每周3小时 中级证书 查看课程 (在新标签页中打开)
  • Coursera上的联邦税收I:个人、雇员与独资经营者 Petro Lisowsky, PhD, CPA 4.8(满分5分)(725条评分) 8周,每周3小时 中级证书 查看课程 (在新标签页中打开)
  • Udemy上的完整GST课程及认证 - 扩展您的CA业务 ClearTax Learning 4.2(满分5分)(1,444条评分) 1天18小时52分钟 初级证书 查看课程 (在新标签页中打开)

更多GST课程请访问Class Central

→

我在healthycalculatorhub.com开发免费健康与健身计算器 - 目前已有33个计算器,涵盖BMI、基础代谢率(BMR)、身体成分、营养宏量和运动节奏。我撰写了这些计算器背后的技术实现:静态网站性能优化、CDN缓存策略,以及验证优化效果的实际测量数据。

如果您读到了这里,请感谢作者以表达您的支持。说声谢谢

免费学习编程。freeCodeCamp的开源课程体系已帮助超过40,000人成功成为开发者。立即开始学习