跳转到内容

提炼变量 / 内联变量

提炼变量(Extract Variable)给一个表达式的某一部分起个名字,存进局部变量;内联变量(Inline Variable)反过来,把只是在转述表达式的变量去掉。 它们解决的痛是:一行算式要在心里拆开才读得懂,或者反过来,一个变量名没有比它右边的表达式多说任何东西。

老陈维护一个批发系统的报价函数。规则是:总价等于单价乘数量,超过 500 件的部分打九五折,运费按总价的 10% 算但最多 100 元。老陈把三条规则写成了一行。

"""批发报价:重构前。"""
def price(order: dict) -> float:
return (order["quantity"] * order["item_price"]
- max(0, order["quantity"] - 500) * order["item_price"] * 0.05
+ min(order["quantity"] * order["item_price"] * 0.1, 100.0))
if __name__ == "__main__":
print(price({"quantity": 600, "item_price": 2.0}))
print(price({"quantity": 100, "item_price": 3.0}))
# 输出:
# 1290.0
# 330.0
// 批发报价:重构前。
interface Order {
quantity: number;
itemPrice: number;
}
export function price(order: Order): number {
return (
order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100)
);
}
console.log(price({ quantity: 600, itemPrice: 2 }));
console.log(price({ quantity: 100, itemPrice: 3 }));
// 输出:
// 1290
// 330

如果现在运营说运费上限从 100 改成 80,你得在这一行里找到那个 100.0,并且确认它是运费上限而不是别的什么 100。这一行里 order["quantity"] * order["item_price"] 出现了两次,哪一次是总价、哪一次是算运费的底数,要对照规则文档才能确定。

更麻烦的是读。三条业务规则被压在一个表达式里,没有一条有名字。新同事看到 max(0, order["quantity"] - 500) 得自己推出“这是超过 500 件的部分”,推错了就会在旁边再加一条错的规则。这是神秘命名:不是名字起得差,而是该有名字的东西根本没有名字。

"""批发报价:重构后。"""
def price(order: dict) -> float:
base_price = order["quantity"] * order["item_price"]
quantity_discount = max(0, order["quantity"] - 500) * order["item_price"] * 0.05
shipping = min(base_price * 0.1, 100.0)
return base_price - quantity_discount + shipping
if __name__ == "__main__":
print(price({"quantity": 600, "item_price": 2.0}))
print(price({"quantity": 100, "item_price": 3.0}))
# 输出:
# 1290.0
# 330.0
// 批发报价:重构后。
interface Order {
quantity: number;
itemPrice: number;
}
export function price(order: Order): number {
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
console.log(price({ quantity: 600, itemPrice: 2 }));
console.log(price({ quantity: 100, itemPrice: 3 }));
// 输出:
// 1290
// 330

三条规则各有了名字。改运费上限,找 shipping 那一行。总价只算一次,第二处直接用 base_price。最后一行 base_price - quantity_discount + shipping 读起来就是规则文档的原话。

  1. 确认要提炼的表达式没有副作用(不改变任何东西,多算一次也无妨)。
  2. 声明一个变量,用表达式的含义命名,把表达式赋给它。
  3. 用这个变量替换原来的表达式。同样的表达式出现多次就都替换掉。
  4. 跑测试。

IDE 的 Extract Variable 命令能完成第 2、3 步,并自动找出重复出现的表达式(IDE 可自动完成)。

内联变量反过来:确认赋值右边的表达式没有副作用,用表达式替换所有用到变量的地方,删掉声明,跑测试。IDE 的 Inline 同样能做(IDE 可自动完成)。

提炼变量:用一个有名字的局部变量替换表达式的一部分,让这部分的意图在名字里。它最常用在三种地方:一行里塞了多条业务规则的算式;条件判断里有三四个 and、or 连起来的布尔表达式;同一个子表达式在一个函数里出现多次。

条件判断是最值得提炼的地方。if order["total"] > 500 and order["customer"]["tier"] == "vip" and not order["returned"] 这样一行,读者要逐个条件在心里翻译;写成 if is_large_vip_order 之后,判断的意图一目了然,条件本身的细节放在上一行,想看再看。

提炼出来的名字只在当前函数里有意义时,提炼成变量;如果这个名字在别的函数里也用得上,比如 base_price 在开发票的函数里也要算,那该提炼的是函数,见第 4 章;如果它是某个对象自己就该知道的事,比如 order.base_price,那该提炼的是方法,见第 16 章以查询取代临时变量。

它的反向手法是内联变量。什么时候反过来做?当变量名和表达式说的是同一件事:

"""内联变量:重构前。"""
def is_expensive(item: dict) -> bool:
base_price = item["price"]
return base_price > 100
if __name__ == "__main__":
print(is_expensive({"price": 150}))
print(is_expensive({"price": 50}))
# 输出:
# True
# False
// 内联变量:重构前。
export function isExpensive(item: { price: number }): boolean {
const basePrice = item.price;
return basePrice > 100;
}
console.log(isExpensive({ price: 150 }));
console.log(isExpensive({ price: 50 }));
// 输出:
// true
// false

base_price 只是给 item["price"] 换了个说法,内联回去更直接:

"""内联变量:重构后。"""
def is_expensive(item: dict) -> bool:
return item["price"] > 100
if __name__ == "__main__":
print(is_expensive({"price": 150}))
print(is_expensive({"price": 50}))
# 输出:
# True
# False
// 内联变量:重构后。
export function isExpensive(item: { price: number }): boolean {
return item.price > 100;
}
console.log(isExpensive({ price: 150 }));
console.log(isExpensive({ price: 50 }));
// 输出:
// true
// false

内联变量还常作为别的重构的准备步骤:要把一段代码提炼成函数时,先把挡路的中间变量内联掉,提炼就干净了。

React 组件常在 JSX 里写 {条件 && <元素/>},条件一长就成了一串 &&。下面用返回字符串的函数模拟组件渲染,badgeV1 是一串条件,badgeV2 把条件提炼成两个有名字的布尔变量。

// 模拟 React 组件:是否显示付费徽章。
interface User {
plan: string;
suspended: boolean;
trialDaysLeft: number;
}
function badgeV1(user: User | null): string {
return user && user.plan === "pro" && !user.suspended && user.trialDaysLeft === 0 ? "<ProBadge/>" : "";
}
function badgeV2(user: User | null): string {
const isActivePro = user !== null && user.plan === "pro" && !user.suspended;
const isPaying = isActivePro && user?.trialDaysLeft === 0;
return isPaying ? "<ProBadge/>" : "";
}
const paying: User = { plan: "pro", suspended: false, trialDaysLeft: 0 };
const trial: User = { plan: "pro", suspended: false, trialDaysLeft: 7 };
console.log(badgeV1(paying) === badgeV2(paying), badgeV2(paying));
console.log(badgeV1(trial) === badgeV2(trial), JSON.stringify(badgeV2(trial)));
console.log(badgeV1(null) === badgeV2(null));
// 输出:
// true <ProBadge/>
// true ""
// true

isActivePro 和 isPaying 这两个名字就是产品规则:活跃的专业版用户,并且试用期已经结束。改规则时改一个变量,JSX 里不用动。

表达式本身一眼能懂时不用提炼,total * 1.06 不需要叫 total_with_tax 再用。提炼出一个只用一次、名字又和表达式差不多长的变量,是在给读者增加一次眼睛的往返,不是减少。提炼出来的名字如果在函数外也有意义,比如 base_price 在别的函数里也要算,那该提炼的是函数不是变量,见第 4 章和第 16 章。

  • 一行里有多条规则,给每条起名字,最后一行就是规则的原话。
  • 同一个子表达式出现两次以上,提炼成变量,算一次用多次。
  • 变量名没有比表达式多说什么时,内联回去。
  • 提炼和内联 IDE 都能一键做,判断留给你。