以查询取代临时变量
以查询取代临时变量(Replace Temp with Query)把一个只保存某个计算结果的临时变量换成一个方法,需要这个值的地方都调用方法。 它解决的痛是:同一个中间值在类的好几个方法里各算一遍,规则一改就得找遍所有临时变量。
订单类 Order 的 price 方法先算基础价,再按基础价是否超过 1000 定折扣系数,最后相乘。后来又加了一个 invoice_note 方法,也要知道有没有打折,于是把这两行又写了一遍。
"""订单价格:重构前。"""
class Order: def __init__(self, quantity: int, item_price: float) -> None: self.quantity = quantity self.item_price = item_price
def price(self) -> float: base_price = self.quantity * self.item_price discount_factor = 0.95 if base_price > 1000 else 0.98 return base_price * discount_factor
def invoice_note(self) -> str: base_price = self.quantity * self.item_price return "大单九五折" if base_price > 1000 else "常规九八折"
if __name__ == "__main__": for order in (Order(100, 20), Order(10, 20)): print(order.price(), order.invoice_note())
# 输出:# 1900.0 大单九五折# 196.0 常规九八折// 订单价格:重构前。class Order { constructor( readonly quantity: number, readonly itemPrice: number, ) {}
price(): number { const basePrice = this.quantity * this.itemPrice; const discountFactor = basePrice > 1000 ? 0.95 : 0.98; return basePrice * discountFactor; }
invoiceNote(): string { const basePrice = this.quantity * this.itemPrice; return basePrice > 1000 ? "大单九五折" : "常规九八折"; }}
for (const order of [new Order(100, 20), new Order(10, 20)]) { console.log(order.price(), order.invoiceNote());}
// 输出:// 1900 大单九五折// 196 常规九八折如果现在大单门槛从 1000 改成 800,要改 price 和 invoice_note 两处,而且两处写法不完全一样(一处算折扣系数,一处算文案),很容易只改一处。基础价的算法 quantity * item_price 也写了两遍,哪天要加上运费或减去优惠券,又是两处。
问题的根子在于 base_price 和“是不是大单”都是这个订单自己的属性,却被写成了某个方法里的局部变量,别的方法看不见,只好自己再算一遍。这是重复代码。
"""订单价格:重构后。"""
class Order: def __init__(self, quantity: int, item_price: float) -> None: self.quantity = quantity self.item_price = item_price
@property def base_price(self) -> float: return self.quantity * self.item_price
@property def is_bulk(self) -> bool: return self.base_price > 1000
@property def discount_factor(self) -> float: return 0.95 if self.is_bulk else 0.98
def price(self) -> float: return self.base_price * self.discount_factor
def invoice_note(self) -> str: return "大单九五折" if self.is_bulk else "常规九八折"
if __name__ == "__main__": for order in (Order(100, 20), Order(10, 20)): print(order.price(), order.invoice_note())
# 输出:# 1900.0 大单九五折# 196.0 常规九八折// 订单价格:重构后。class Order { constructor( readonly quantity: number, readonly itemPrice: number, ) {}
get basePrice(): number { return this.quantity * this.itemPrice; }
get isBulk(): boolean { return this.basePrice > 1000; }
get discountFactor(): number { return this.isBulk ? 0.95 : 0.98; }
price(): number { return this.basePrice * this.discountFactor; }
invoiceNote(): string { return this.isBulk ? "大单九五折" : "常规九八折"; }}
for (const order of [new Order(100, 20), new Order(10, 20)]) { console.log(order.price(), order.invoiceNote());}
// 输出:// 1900 大单九五折// 196 常规九八折门槛改成 800 只改 is_bulk 一处,price 和 invoice_note 都跟着变。基础价只写一次。price 本身也短到一行,读起来就是“基础价乘折扣系数”。
怎么一步步做
Section titled “怎么一步步做”- 确认临时变量只被赋值一次。如果被赋值了好几次,先用拆分变量(第 30 章)把它拆开。
- 确认赋值右边的表达式没有副作用,每次调用都返回同样的结果。
- 用提炼函数(第 4 章)把赋值右边的表达式提成方法。IDE 的 Extract Method 能完成提炼(IDE 可自动完成),之后手工加上 Python 的
@property或 TS 的get,调用处看起来仍像在读一个字段。 - 跑测试。
- 用内联变量(第 5 章)去掉临时变量,让用到它的地方直接调用方法。跑测试。
- 在别的方法里找同样的计算,换成调用这个方法。
提出来的查询方法名字要说清它算的是什么,而不是怎么算。base_price 比 quantity_times_price 好,is_bulk 比 over_1000 好:前者在门槛改成 800 之后仍然准确,后者就成了谎话。
定义与适用场景
Section titled “定义与适用场景”以查询取代临时变量:把一个临时变量的计算提成一个方法,并用方法调用替换对这个变量的所有引用。它只在类里才真正有用,因为类的方法之间共享 self,提出来的方法所有兄弟方法都能用;一个独立函数里的临时变量提成另一个独立函数,还得把参数一个个传进去,通常不如保留临时变量。
它常常是提炼函数的准备步骤:一个长方法里的临时变量越多,越难把一段代码提出去,因为每个临时变量都要变成参数或返回值。先把临时变量换成查询,再提炼,参数列表会短很多。
真实系统里的例子
Section titled “真实系统里的例子”Django 模型上的计算属性
Section titled “Django 模型上的计算属性”Django 项目里最常见的写法就是在模型上加 @property,把视图、模板里反复出现的计算收进模型。下面模拟一个订单行模型,subtotal 和 is_free_shipping 原来分别写在视图和模板里。
"""模拟 Django 模型的计算属性。"""
class OrderLine: FREE_SHIPPING_MIN = 99
def __init__(self, unit_price: float, qty: int) -> None: self.unit_price = unit_price self.qty = qty
@property def subtotal(self) -> float: return self.unit_price * self.qty
@property def is_free_shipping(self) -> bool: return self.subtotal >= self.FREE_SHIPPING_MIN
def cart_view(lines: list[OrderLine]) -> list[str]: return [f"小计 {line.subtotal:.0f},{'包邮' if line.is_free_shipping else '不包邮'}" for line in lines]
if __name__ == "__main__": print(cart_view([OrderLine(68, 2), OrderLine(15, 3)]))
# 输出:# ['小计 136,包邮', '小计 45,不包邮']模板里写 {{ line.subtotal }} 和 {% if line.is_free_shipping %} 就能用,包邮门槛改了只动模型。
@property 还隐含了一个约定:读属性不应该有副作用,也不应该很慢。调用方看到 order.base_price 会以为它像读字段一样便宜,可以在循环里随便读。如果提出来的计算其实要查数据库或者算几秒钟,就别用属性,用一个名字里带动词的普通方法,比如 calculate_base_price(),让调用方一看就知道这是一次计算。
什么时候别用
Section titled “什么时候别用”计算很贵(查数据库、调接口)、又在一个方法里用了好几次,换成查询会让它被算好几遍。这时保留临时变量,或者用 Python 的 functools.cached_property 缓存结果;但缓存意味着对象变了它不会更新,只适合不可变的对象。
临时变量在被赋值之后又被修改(比如循环里累加),它不是“某个计算的结果”,而是一个累加器,不适用本手法。
提出来的查询方法越多,类的行为就越完整,调用方需要自己拼凑的计算就越少。
- 类里一个临时变量只是某个计算的结果、别的方法也要用,就把它提成方法。
- Python 用
@property、TS 用 getter,调用处读起来仍像字段。 - 先确认只赋值一次、没有副作用,再提炼、再内联掉临时变量。
- 它常是提炼函数的准备步骤:临时变量少了,提出去的函数参数也少。
- 计算很贵时保留临时变量或加缓存。