Replace Derived Variable with Query 是什么
删除可由其它数据推导出来的存储字段,改为计算查询。存储派生值意味着每个改动源都要记得同步更新它,漏掉一处就会出现不一致;改成实时计算后,这类同步点直接消失。只有在计算确实昂贵且调用频繁时,才用显式缓存替代隐式冗余,并且必须把失效策略写清楚。
基本信息
| 收益 | 消除同步点与不一致窗口 |
|---|---|
| 例外 | 昂贵计算可缓存,但需明确失效策略 |
| 场景 | 合计金额、条目计数、状态派生值、剩余额度 |
| 配合 | 把不变的输入封装好,查询表达式保持纯函数 |
判断线索
- 同一个数在多个地方赋值,改动源分散且要靠人记住
- 字段值与其它字段存在固定关系,但没有任何地方强制校验
- 出现「忘记更新派生字段」这类缺陷,且复查时难以定位
- 字段只在读取前一刻才需要,却一直保存在对象上
- 序列化时派生的计数被一起存下去,恢复后可能已经过期
Java 示例
Java 示例
// 之前:discountedTotal 是字段,每个改动路径都要记得重算
class Order {
private Money discountedTotal;
void applyCoupon(Coupon c) {
items.forEach(i -> i.apply(c));
discountedTotal = computeTotal(); // 少写这一行就出不一致
}
}
// 之后:实时计算,不存在同步问题
double discountedTotal() {
return baseTotal() - discount();
}
// 昂贵且高频时,显式缓存并管理失效
private Money cached;
Money total() {
if (cached == null) cached = computeTotal();
return cached;
}
void invalidate() { cached = null; } // 每个改动点显式调用三种处理方式
| 方式 | 一致性风险 | 适用 |
|---|---|---|
| 实时计算 | 无 | 计算便宜,或调用频率低 |
| 显式缓存加失效 | 中,依赖失效点完整 | 计算昂贵且读取频繁 |
| 存储字段加同步 | 高,靠人记住 | 几乎不推荐 |
| 数据库物化 | 中,靠触发或作业 | 跨实例共享的统计口径 |
缓存失效逻辑写错,代价比不缓存更大:它会把错误结果稳定地返回给所有调用方,而且看起来一切正常。改动频繁的对象上,宁可实时计算,等真出现性能问题再引入缓存并配测试。
热点路径每次全量重算时,先度量调用频率与单次耗时:很多「性能顾虑」在数据上并不成立,反而去掉派生字段能省下同步更新的复杂度与缺陷成本。