首页/开发/代码质量与评审速查/手法:Replace Derived Variable with Query(派生值改为查询)
重构手法62 条

手法:Replace Derived Variable with Query(派生值改为查询)

删除可由其他数据推导的存储字段:改为计算查询后不存在同步问题;性能敏感且计算昂贵时,用显式缓存并管理失效,而不是隐式冗余。

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; }   // 每个改动点显式调用

三种处理方式

方式一致性风险适用
实时计算无计算便宜,或调用频率低
显式缓存加失效中,依赖失效点完整计算昂贵且读取频繁
存储字段加同步高,靠人记住几乎不推荐
数据库物化中,靠触发或作业跨实例共享的统计口径
缓存失效逻辑写错,代价比不缓存更大:它会把错误结果稳定地返回给所有调用方,而且看起来一切正常。改动频繁的对象上,宁可实时计算,等真出现性能问题再引入缓存并配测试。
热点路径每次全量重算时,先度量调用频率与单次耗时:很多「性能顾虑」在数据上并不成立,反而去掉派生字段能省下同步更新的复杂度与缺陷成本。

易错点

  • 热点路径每次全量重算:改前先度量调用频率,不要在猜想的性能问题上反复权衡
  • 缓存失效逻辑写错:不如不缓存,退回实时计算更安全
  • 只删字段不删赋值语句:残留的写入让读者以为该值仍然重要
  • 跨实例缓存放在进程内:多副本部署下各实例结果不同,用户看到数字跳变

数据来源与参考

  • 《重构》第 2 版 · 第 9 章(Replace Derived Variable with Query)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌