提炼变量 / 内联变量
提炼变量(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 读起来就是规则文档的原话。
怎么一步步做
Section titled “怎么一步步做”- 确认要提炼的表达式没有副作用(不改变任何东西,多算一次也无妨)。
- 声明一个变量,用表达式的含义命名,把表达式赋给它。
- 用这个变量替换原来的表达式。同样的表达式出现多次就都替换掉。
- 跑测试。
IDE 的 Extract Variable 命令能完成第 2、3 步,并自动找出重复出现的表达式(IDE 可自动完成)。
内联变量反过来:确认赋值右边的表达式没有副作用,用表达式替换所有用到变量的地方,删掉声明,跑测试。IDE 的 Inline 同样能做(IDE 可自动完成)。
定义与适用场景
Section titled “定义与适用场景”提炼变量:用一个有名字的局部变量替换表达式的一部分,让这部分的意图在名字里。它最常用在三种地方:一行里塞了多条业务规则的算式;条件判断里有三四个 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// falsebase_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内联变量还常作为别的重构的准备步骤:要把一段代码提炼成函数时,先把挡路的中间变量内联掉,提炼就干净了。
真实系统里的例子
Section titled “真实系统里的例子”React 组件里的条件渲染
Section titled “React 组件里的条件渲染”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 ""// trueisActivePro 和 isPaying 这两个名字就是产品规则:活跃的专业版用户,并且试用期已经结束。改规则时改一个变量,JSX 里不用动。
什么时候别用
Section titled “什么时候别用”表达式本身一眼能懂时不用提炼,total * 1.06 不需要叫 total_with_tax 再用。提炼出一个只用一次、名字又和表达式差不多长的变量,是在给读者增加一次眼睛的往返,不是减少。提炼出来的名字如果在函数外也有意义,比如 base_price 在别的函数里也要算,那该提炼的是函数不是变量,见第 4 章和第 16 章。
- 一行里有多条规则,给每条起名字,最后一行就是规则的原话。
- 同一个子表达式出现两次以上,提炼成变量,算一次用多次。
- 变量名没有比表达式多说什么时,内联回去。
- 提炼和内联 IDE 都能一键做,判断留给你。