跳转到内容

以查询取代临时变量

以查询取代临时变量(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 本身也短到一行,读起来就是“基础价乘折扣系数”。

  1. 确认临时变量只被赋值一次。如果被赋值了好几次,先用拆分变量(第 30 章)把它拆开。
  2. 确认赋值右边的表达式没有副作用,每次调用都返回同样的结果。
  3. 用提炼函数(第 4 章)把赋值右边的表达式提成方法。IDE 的 Extract Method 能完成提炼(IDE 可自动完成),之后手工加上 Python 的 @property 或 TS 的 get,调用处看起来仍像在读一个字段。
  4. 跑测试。
  5. 用内联变量(第 5 章)去掉临时变量,让用到它的地方直接调用方法。跑测试。
  6. 在别的方法里找同样的计算,换成调用这个方法。

提出来的查询方法名字要说清它算的是什么,而不是怎么算。base_price 比 quantity_times_price 好,is_bulk 比 over_1000 好:前者在门槛改成 800 之后仍然准确,后者就成了谎话。

以查询取代临时变量:把一个临时变量的计算提成一个方法,并用方法调用替换对这个变量的所有引用。它只在类里才真正有用,因为类的方法之间共享 self,提出来的方法所有兄弟方法都能用;一个独立函数里的临时变量提成另一个独立函数,还得把参数一个个传进去,通常不如保留临时变量。

它常常是提炼函数的准备步骤:一个长方法里的临时变量越多,越难把一段代码提出去,因为每个临时变量都要变成参数或返回值。先把临时变量换成查询,再提炼,参数列表会短很多。

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(),让调用方一看就知道这是一次计算。

计算很贵(查数据库、调接口)、又在一个方法里用了好几次,换成查询会让它被算好几遍。这时保留临时变量,或者用 Python 的 functools.cached_property 缓存结果;但缓存意味着对象变了它不会更新,只适合不可变的对象。

临时变量在被赋值之后又被修改(比如循环里累加),它不是“某个计算的结果”,而是一个累加器,不适用本手法。

提出来的查询方法越多,类的行为就越完整,调用方需要自己拼凑的计算就越少。

  • 类里一个临时变量只是某个计算的结果、别的方法也要用,就把它提成方法。
  • Python 用 @property、TS 用 getter,调用处读起来仍像字段。
  • 先确认只赋值一次、没有副作用,再提炼、再内联掉临时变量。
  • 它常是提炼函数的准备步骤:临时变量少了,提出去的函数参数也少。
  • 计算很贵时保留临时变量或加缓存。