跳转到内容

函数上移 / 函数下移

本章讲五个手法:函数上移、字段上移、构造函数本体上移、函数下移、字段下移。它们在父类和子类之间搬东西:几个子类里一样的函数、字段、构造函数开头搬到父类,叫上移;父类里只有一个子类用得上的函数、字段搬到那个子类,叫下移。 它们解决的痛是:同一段代码在每个子类里抄一遍,改一处漏一处;或者父类替某一个子类背着东西,其他子类被迫继承一堆用不上、还可能用错的成员。

小林接手公司的人事系统。员工分工程师和销售两类,代码里是父类 Employee 和两个子类 Engineer、Salesman。两个子类是不同的人在不同时间写的:先有工程师,后来有人照着工程师抄了一份改成销售;再后来为了算销售的业绩,又有人图省事把“销售指标”和“提成”直接加在了 Employee 上。现在要加第三类员工:经理。

下面五段代码各对应一个手法,都取自这套人事系统的不同角落,每段只保留和该手法有关的部分。

两个子类各自存一个 name。花名册函数要列出所有人的名字,参数类型只能写成“工程师或销售”,因为 Employee 本身没有 name。

"""字段上移:重构前。"""
from abc import ABC, abstractmethod
class Employee(ABC):
@abstractmethod
def role(self) -> str: ...
class Engineer(Employee):
def __init__(self, name: str, level: int) -> None:
self.name = name
self.level = level
def role(self) -> str:
return f"{self.level} 级工程师"
class Salesman(Employee):
def __init__(self, name: str, region: str) -> None:
self.name = name
self.region = region
def role(self) -> str:
return f"{self.region}区销售"
def roster(staff: list[Engineer | Salesman]) -> list[str]:
return [f"{e.name}:{e.role()}" for e in staff]
if __name__ == "__main__":
for line in roster([Engineer("张伟", 3), Salesman("李娜", "华东")]):
print(line)
# 输出:
# 张伟:3 级工程师
# 李娜:华东区销售
// 字段上移:重构前。
export abstract class Employee {
abstract role(): string;
}
export class Engineer extends Employee {
constructor(
readonly name: string,
readonly level: number,
) {
super();
}
role(): string {
return `${this.level} 级工程师`;
}
}
export class Salesman extends Employee {
constructor(
readonly name: string,
readonly region: string,
) {
super();
}
role(): string {
return `${this.region}区销售`;
}
}
export function roster(staff: (Engineer | Salesman)[]): string[] {
return staff.map((e) => `${e.name}:${e.role()}`);
}
for (const line of roster([new Engineer("张伟", 3), new Salesman("李娜", "华东")])) {
console.log(line);
}
// 输出:
// 张伟:3 级工程师
// 李娜:华东区销售
import unittest
from target import Engineer, Salesman, roster
class RosterTest(unittest.TestCase):
def test_lists_name_and_role(self):
staff = [Engineer("张伟", 3), Salesman("李娜", "华东")]
self.assertEqual(roster(staff), ["张伟:3 级工程师", "李娜:华东区销售"])
def test_every_employee_has_a_name(self):
self.assertEqual(Engineer("王强", 2).name, "王强")
self.assertEqual(Salesman("赵敏", "华南").name, "赵敏")
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { Engineer, Salesman, roster } from "./target";
test("花名册列出名字和角色", () => {
const staff = [new Engineer("张伟", 3), new Salesman("李娜", "华东")];
assert.deepEqual(roster(staff), ["张伟:3 级工程师", "李娜:华东区销售"]);
});
test("每个员工都有名字", () => {
assert.equal(new Engineer("王强", 2).name, "王强");
assert.equal(new Salesman("赵敏", "华南").name, "赵敏");
});

Employee 已经管着名字,但两个子类的构造函数里还各有一段一模一样的月成本校验和赋值。

"""构造函数本体上移:重构前。"""
class Employee:
def __init__(self, name: str) -> None:
self.name = name
class Engineer(Employee):
def __init__(self, name: str, monthly_cost: int, level: int) -> None:
super().__init__(name)
if monthly_cost <= 0:
raise ValueError(f"{name}的月成本必须是正数")
self.monthly_cost = monthly_cost
self.level = level
class Salesman(Employee):
def __init__(self, name: str, monthly_cost: int, region: str) -> None:
super().__init__(name)
if monthly_cost <= 0:
raise ValueError(f"{name}的月成本必须是正数")
self.monthly_cost = monthly_cost
self.region = region
if __name__ == "__main__":
print(Engineer("张伟", 30000, 3).monthly_cost)
try:
Salesman("李娜", 0, "华东")
except ValueError as error:
print(error)
# 输出:
# 30000
# 李娜的月成本必须是正数
// 构造函数本体上移:重构前。
export class Employee {
constructor(readonly name: string) {}
}
export class Engineer extends Employee {
readonly monthlyCost: number;
constructor(
name: string,
monthlyCost: number,
readonly level: number,
) {
super(name);
if (monthlyCost <= 0) throw new Error(`${name}的月成本必须是正数`);
this.monthlyCost = monthlyCost;
}
}
export class Salesman extends Employee {
readonly monthlyCost: number;
constructor(
name: string,
monthlyCost: number,
readonly region: string,
) {
super(name);
if (monthlyCost <= 0) throw new Error(`${name}的月成本必须是正数`);
this.monthlyCost = monthlyCost;
}
}
console.log(new Engineer("张伟", 30000, 3).monthlyCost);
try {
new Salesman("李娜", 0, "华东");
} catch (error) {
if (error instanceof Error) console.log(error.message);
}
// 输出:
// 30000
// 李娜的月成本必须是正数
import unittest
from target import Engineer, Salesman
class MonthlyCostTest(unittest.TestCase):
def test_keeps_monthly_cost(self):
self.assertEqual(Engineer("张伟", 30000, 3).monthly_cost, 30000)
self.assertEqual(Salesman("李娜", 20000, "华东").monthly_cost, 20000)
def test_rejects_non_positive_cost(self):
with self.assertRaisesRegex(ValueError, "李娜的月成本必须是正数"):
Salesman("李娜", 0, "华东")
with self.assertRaisesRegex(ValueError, "张伟的月成本必须是正数"):
Engineer("张伟", -1, 3)
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { Engineer, Salesman } from "./target";
test("记住月成本", () => {
assert.equal(new Engineer("张伟", 30000, 3).monthlyCost, 30000);
assert.equal(new Salesman("李娜", 20000, "华东").monthlyCost, 20000);
});
test("月成本不是正数就拒绝", () => {
assert.throws(() => new Salesman("李娜", 0, "华东"), /李娜的月成本必须是正数/);
assert.throws(() => new Engineer("张伟", -1, 3), /张伟的月成本必须是正数/);
});

两个子类各有一个 annual_cost。写法略有不同,一个是 monthly_cost * 12,一个是 12 * monthly_cost,算的是同一件事。

"""函数上移:重构前。"""
class Employee:
def __init__(self, name: str, monthly_cost: int) -> None:
self.name = name
self.monthly_cost = monthly_cost
class Engineer(Employee):
def __init__(self, name: str, monthly_cost: int, level: int) -> None:
super().__init__(name, monthly_cost)
self.level = level
def annual_cost(self) -> int:
return self.monthly_cost * 12
class Salesman(Employee):
def __init__(self, name: str, monthly_cost: int, region: str) -> None:
super().__init__(name, monthly_cost)
self.region = region
def annual_cost(self) -> int:
return 12 * self.monthly_cost
def total_cost(staff: list[Engineer | Salesman]) -> int:
return sum(e.annual_cost() for e in staff)
if __name__ == "__main__":
staff = [Engineer("张伟", 30000, 3), Salesman("李娜", 20000, "华东")]
for e in staff:
print(e.name, e.annual_cost())
print("合计", total_cost(staff))
# 输出:
# 张伟 360000
# 李娜 240000
# 合计 600000
// 函数上移:重构前。
export class Employee {
constructor(
readonly name: string,
readonly monthlyCost: number,
) {}
}
export class Engineer extends Employee {
constructor(
name: string,
monthlyCost: number,
readonly level: number,
) {
super(name, monthlyCost);
}
annualCost(): number {
return this.monthlyCost * 12;
}
}
export class Salesman extends Employee {
constructor(
name: string,
monthlyCost: number,
readonly region: string,
) {
super(name, monthlyCost);
}
annualCost(): number {
return 12 * this.monthlyCost;
}
}
export function totalCost(staff: (Engineer | Salesman)[]): number {
return staff.reduce((sum, e) => sum + e.annualCost(), 0);
}
const staff = [new Engineer("张伟", 30000, 3), new Salesman("李娜", 20000, "华东")];
for (const e of staff) {
console.log(e.name, e.annualCost());
}
console.log("合计", totalCost(staff));
// 输出:
// 张伟 360000
// 李娜 240000
// 合计 600000
import unittest
from target import Engineer, Salesman, total_cost
class AnnualCostTest(unittest.TestCase):
def test_annual_cost_is_twelve_months(self):
self.assertEqual(Engineer("张伟", 30000, 3).annual_cost(), 360000)
self.assertEqual(Salesman("李娜", 20000, "华东").annual_cost(), 240000)
def test_total_cost(self):
staff = [Engineer("张伟", 30000, 3), Salesman("李娜", 20000, "华东")]
self.assertEqual(total_cost(staff), 600000)
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { Engineer, Salesman, totalCost } from "./target";
test("年成本是十二个月", () => {
assert.equal(new Engineer("张伟", 30000, 3).annualCost(), 360000);
assert.equal(new Salesman("李娜", 20000, "华东").annualCost(), 240000);
});
test("合计", () => {
assert.equal(totalCost([new Engineer("张伟", 30000, 3), new Salesman("李娜", 20000, "华东")]), 600000);
});

提成只有销售有,算提成的方法却写在 Employee 上,于是每个工程师对象也带着一个 commission。

"""函数下移:重构前。"""
class Employee:
def __init__(self, name: str) -> None:
self.name = name
def commission(self, sales: int) -> float:
return sales * 3 / 100
class Engineer(Employee):
def __init__(self, name: str, level: int) -> None:
super().__init__(name)
self.level = level
class Salesman(Employee):
def __init__(self, name: str, region: str) -> None:
super().__init__(name)
self.region = region
def commission_report(team: list[Salesman], sales: dict[str, int]) -> list[str]:
return [f"{s.name} 提成 {s.commission(sales[s.name]):.2f}" for s in team]
if __name__ == "__main__":
team = [Salesman("李娜", "华东"), Salesman("赵敏", "华南")]
for line in commission_report(team, {"李娜": 100000, "赵敏": 80000}):
print(line)
# 输出:
# 李娜 提成 3000.00
# 赵敏 提成 2400.00
// 函数下移:重构前。
export class Employee {
constructor(readonly name: string) {}
commission(sales: number): number {
return (sales * 3) / 100;
}
}
export class Engineer extends Employee {
constructor(
name: string,
readonly level: number,
) {
super(name);
}
}
export class Salesman extends Employee {
constructor(
name: string,
readonly region: string,
) {
super(name);
}
}
export function commissionReport(team: Salesman[], sales: Record<string, number>): string[] {
return team.map((s) => `${s.name} 提成 ${s.commission(sales[s.name]).toFixed(2)}`);
}
const team = [new Salesman("李娜", "华东"), new Salesman("赵敏", "华南")];
for (const line of commissionReport(team, { 李娜: 100000, 赵敏: 80000 })) {
console.log(line);
}
// 输出:
// 李娜 提成 3000.00
// 赵敏 提成 2400.00
import unittest
from target import Salesman, commission_report
class CommissionTest(unittest.TestCase):
def test_three_percent_of_sales(self):
self.assertEqual(Salesman("李娜", "华东").commission(100000), 3000)
def test_report(self):
team = [Salesman("李娜", "华东"), Salesman("赵敏", "华南")]
report = commission_report(team, {"李娜": 100000, "赵敏": 80000})
self.assertEqual(report, ["李娜 提成 3000.00", "赵敏 提成 2400.00"])
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { Salesman, commissionReport } from "./target";
test("提成是销售额的 3%", () => {
assert.equal(new Salesman("李娜", "华东").commission(100000), 3000);
});
test("提成报表", () => {
const team = [new Salesman("李娜", "华东"), new Salesman("赵敏", "华南")];
assert.deepEqual(commissionReport(team, { 李娜: 100000, 赵敏: 80000 }), ["李娜 提成 3000.00", "赵敏 提成 2400.00"]);
});

销售指标 quota 也放在了 Employee 上,默认值 0。工程师没有指标,却人人带着一个 quota = 0。

"""字段下移:重构前。"""
class Employee:
def __init__(self, name: str, quota: int = 0) -> None:
self.name = name
self.quota = quota
class Engineer(Employee):
def __init__(self, name: str, level: int) -> None:
super().__init__(name)
self.level = level
class Salesman(Employee):
def __init__(self, name: str, region: str, quota: int) -> None:
super().__init__(name, quota)
self.region = region
def quota_met(self, sales: int) -> bool:
return sales >= self.quota
if __name__ == "__main__":
li = Salesman("李娜", "华东", 500000)
print(li.name, li.quota_met(600000), li.quota_met(400000))
print(Engineer("张伟", 3).name)
# 输出:
# 李娜 True False
# 张伟
// 字段下移:重构前。
export class Employee {
constructor(
readonly name: string,
readonly quota: number = 0,
) {}
}
export class Engineer extends Employee {
constructor(
name: string,
readonly level: number,
) {
super(name);
}
}
export class Salesman extends Employee {
constructor(
name: string,
readonly region: string,
quota: number,
) {
super(name, quota);
}
quotaMet(sales: number): boolean {
return sales >= this.quota;
}
}
const li = new Salesman("李娜", "华东", 500000);
console.log(li.name, li.quotaMet(600000), li.quotaMet(400000));
console.log(new Engineer("张伟", 3).name);
// 输出:
// 李娜 true false
// 张伟
import unittest
from target import Engineer, Salesman
class QuotaTest(unittest.TestCase):
def test_quota_met(self):
li = Salesman("李娜", "华东", 500000)
self.assertTrue(li.quota_met(500000))
self.assertFalse(li.quota_met(499999))
def test_salesman_keeps_quota(self):
self.assertEqual(Salesman("李娜", "华东", 500000).quota, 500000)
def test_engineer_still_works(self):
self.assertEqual(Engineer("张伟", 3).level, 3)
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { Engineer, Salesman } from "./target";
test("是否完成指标", () => {
const li = new Salesman("李娜", "华东", 500000);
assert.equal(li.quotaMet(500000), true);
assert.equal(li.quotaMet(499999), false);
});
test("销售记得自己的指标", () => {
assert.equal(new Salesman("李娜", "华东", 500000).quota, 500000);
});
test("工程师照常构造", () => {
assert.equal(new Engineer("张伟", 3).level, 3);
});

如果现在要加经理 Manager,小林得把下面这些事做一遍。

先看上移那一侧。经理也有名字,于是 Manager 的构造函数里再写一次 self.name = name;花名册 roster 和成本合计 total_cost 的参数类型从“工程师或销售”改成“工程师或销售或经理”,有几个这样的函数就要改几处,漏改一个,Python 的类型检查器报错,TS 直接编译不过。经理也有月成本,于是那段校验再抄一份;哪天财务说月成本可以为 0(比如停薪留职),要改的是三个构造函数,改了两个、漏了一个,就会出现“工程师可以填 0、经理不行”的怪事。annual_cost 也要抄第三份,而且下一个抄的人看到两份写法不同的版本,会犹豫:monthly_cost * 12 和 12 * monthly_cost 是不是故意不同?要读三遍才敢确定它们一样。

上移前:Engineer 和 Salesman 各有一个 annual_cost,Employee 只有字段

再看下移那一侧。经理从 Employee 继承来一个 commission 和一个 quota = 0。对经理调用 commission(1000000) 会安安静静地返回 30000,没有任何报错,谁要是写个循环给“所有员工”算提成,经理和工程师也会领到一笔。quota 更麻烦:0 到底是“没有指标”还是“指标为零”?已经有人在报表里写了 if e.quota == 0 来判断谁是工程师,加了经理之后这个判断就把经理也算了进去。读 Employee 的人还会被误导,以为“每个员工都有销售指标”是公司的规则。

下移前:quota 字段放在 Employee 上,Engineer 也继承了它,只有 Salesman 的 quota_met 会用

用第 2 章的名字说,上移那一侧是重复代码,下移那一侧是被拒绝的遗赠:子类继承了父类的东西,却用不上也不想要。

"""字段上移:重构后。"""
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name: str) -> None:
self.name = name
@abstractmethod
def role(self) -> str: ...
class Engineer(Employee):
def __init__(self, name: str, level: int) -> None:
super().__init__(name)
self.level = level
def role(self) -> str:
return f"{self.level} 级工程师"
class Salesman(Employee):
def __init__(self, name: str, region: str) -> None:
super().__init__(name)
self.region = region
def role(self) -> str:
return f"{self.region}区销售"
def roster(staff: list[Employee]) -> list[str]:
return [f"{e.name}:{e.role()}" for e in staff]
if __name__ == "__main__":
for line in roster([Engineer("张伟", 3), Salesman("李娜", "华东")]):
print(line)
# 输出:
# 张伟:3 级工程师
# 李娜:华东区销售
// 字段上移:重构后。
export abstract class Employee {
constructor(readonly name: string) {}
abstract role(): string;
}
export class Engineer extends Employee {
constructor(
name: string,
readonly level: number,
) {
super(name);
}
role(): string {
return `${this.level} 级工程师`;
}
}
export class Salesman extends Employee {
constructor(
name: string,
readonly region: string,
) {
super(name);
}
role(): string {
return `${this.region}区销售`;
}
}
export function roster(staff: Employee[]): string[] {
return staff.map((e) => `${e.name}:${e.role()}`);
}
for (const line of roster([new Engineer("张伟", 3), new Salesman("李娜", "华东")])) {
console.log(line);
}
// 输出:
// 张伟:3 级工程师
// 李娜:华东区销售

roster 的参数变成了 list[Employee]。加经理时它一个字不用改,经理只要是 Employee 就有名字。在 Python 和 TS 里,字段是在构造函数里诞生的,所以字段上移通常连带着把赋值语句也搬进父类构造函数,子类改成调用 super。

"""构造函数本体上移:重构后。"""
class Employee:
def __init__(self, name: str, monthly_cost: int) -> None:
if monthly_cost <= 0:
raise ValueError(f"{name}的月成本必须是正数")
self.name = name
self.monthly_cost = monthly_cost
class Engineer(Employee):
def __init__(self, name: str, monthly_cost: int, level: int) -> None:
super().__init__(name, monthly_cost)
self.level = level
class Salesman(Employee):
def __init__(self, name: str, monthly_cost: int, region: str) -> None:
super().__init__(name, monthly_cost)
self.region = region
if __name__ == "__main__":
print(Engineer("张伟", 30000, 3).monthly_cost)
try:
Salesman("李娜", 0, "华东")
except ValueError as error:
print(error)
# 输出:
# 30000
# 李娜的月成本必须是正数
// 构造函数本体上移:重构后。
export class Employee {
constructor(
readonly name: string,
readonly monthlyCost: number,
) {
if (monthlyCost <= 0) throw new Error(`${name}的月成本必须是正数`);
}
}
export class Engineer extends Employee {
constructor(
name: string,
monthlyCost: number,
readonly level: number,
) {
super(name, monthlyCost);
}
}
export class Salesman extends Employee {
constructor(
name: string,
monthlyCost: number,
readonly region: string,
) {
super(name, monthlyCost);
}
}
console.log(new Engineer("张伟", 30000, 3).monthlyCost);
try {
new Salesman("李娜", 0, "华东");
} catch (error) {
if (error instanceof Error) console.log(error.message);
}
// 输出:
// 30000
// 李娜的月成本必须是正数

校验规则只剩一份。财务改口说月成本可以为 0,改 Employee 一处,三类员工同时生效。子类构造函数只剩下自己独有的那一个字段。

"""函数上移:重构后。"""
class Employee:
def __init__(self, name: str, monthly_cost: int) -> None:
self.name = name
self.monthly_cost = monthly_cost
def annual_cost(self) -> int:
return self.monthly_cost * 12
class Engineer(Employee):
def __init__(self, name: str, monthly_cost: int, level: int) -> None:
super().__init__(name, monthly_cost)
self.level = level
class Salesman(Employee):
def __init__(self, name: str, monthly_cost: int, region: str) -> None:
super().__init__(name, monthly_cost)
self.region = region
def total_cost(staff: list[Employee]) -> int:
return sum(e.annual_cost() for e in staff)
if __name__ == "__main__":
staff = [Engineer("张伟", 30000, 3), Salesman("李娜", 20000, "华东")]
for e in staff:
print(e.name, e.annual_cost())
print("合计", total_cost(staff))
# 输出:
# 张伟 360000
# 李娜 240000
# 合计 600000
// 函数上移:重构后。
export class Employee {
constructor(
readonly name: string,
readonly monthlyCost: number,
) {}
annualCost(): number {
return this.monthlyCost * 12;
}
}
export class Engineer extends Employee {
constructor(
name: string,
monthlyCost: number,
readonly level: number,
) {
super(name, monthlyCost);
}
}
export class Salesman extends Employee {
constructor(
name: string,
monthlyCost: number,
readonly region: string,
) {
super(name, monthlyCost);
}
}
export function totalCost(staff: Employee[]): number {
return staff.reduce((sum, e) => sum + e.annualCost(), 0);
}
const staff = [new Engineer("张伟", 30000, 3), new Salesman("李娜", 20000, "华东")];
for (const e of staff) {
console.log(e.name, e.annualCost());
}
console.log("合计", totalCost(staff));
// 输出:
// 张伟 360000
// 李娜 240000
// 合计 600000

上移后:annual_cost 住在 Employee 里,两个子类只剩各自独有的 level 和 region

annual_cost 只剩一份,经理直接继承。total_cost 也能接收 list[Employee] 了。注意上移的前提:TS 里父类方法要用 this.monthlyCost,父类必须先有这个字段,否则编译不过。所以三个上移手法常按“字段、构造函数、函数”的顺序做,前一步给后一步铺路。

"""函数下移:重构后。"""
class Employee:
def __init__(self, name: str) -> None:
self.name = name
class Engineer(Employee):
def __init__(self, name: str, level: int) -> None:
super().__init__(name)
self.level = level
class Salesman(Employee):
def __init__(self, name: str, region: str) -> None:
super().__init__(name)
self.region = region
def commission(self, sales: int) -> float:
return sales * 3 / 100
def commission_report(team: list[Salesman], sales: dict[str, int]) -> list[str]:
return [f"{s.name} 提成 {s.commission(sales[s.name]):.2f}" for s in team]
if __name__ == "__main__":
team = [Salesman("李娜", "华东"), Salesman("赵敏", "华南")]
for line in commission_report(team, {"李娜": 100000, "赵敏": 80000}):
print(line)
# 输出:
# 李娜 提成 3000.00
# 赵敏 提成 2400.00
// 函数下移:重构后。
export class Employee {
constructor(readonly name: string) {}
}
export class Engineer extends Employee {
constructor(
name: string,
readonly level: number,
) {
super(name);
}
}
export class Salesman extends Employee {
constructor(
name: string,
readonly region: string,
) {
super(name);
}
commission(sales: number): number {
return (sales * 3) / 100;
}
}
export function commissionReport(team: Salesman[], sales: Record<string, number>): string[] {
return team.map((s) => `${s.name} 提成 ${s.commission(sales[s.name]).toFixed(2)}`);
}
const team = [new Salesman("李娜", "华东"), new Salesman("赵敏", "华南")];
for (const line of commissionReport(team, { 李娜: 100000, 赵敏: 80000 })) {
console.log(line);
}
// 输出:
// 李娜 提成 3000.00
// 赵敏 提成 2400.00

工程师和以后的经理不再有 commission。有人对工程师调用它,Python 抛 AttributeError,TS 在编译时就报错,“给工程师算提成”这种错误从静默变成了响亮。

"""字段下移:重构后。"""
class Employee:
def __init__(self, name: str) -> None:
self.name = name
class Engineer(Employee):
def __init__(self, name: str, level: int) -> None:
super().__init__(name)
self.level = level
class Salesman(Employee):
def __init__(self, name: str, region: str, quota: int) -> None:
super().__init__(name)
self.region = region
self.quota = quota
def quota_met(self, sales: int) -> bool:
return sales >= self.quota
if __name__ == "__main__":
li = Salesman("李娜", "华东", 500000)
print(li.name, li.quota_met(600000), li.quota_met(400000))
print(Engineer("张伟", 3).name)
# 输出:
# 李娜 True False
# 张伟
// 字段下移:重构后。
export class Employee {
constructor(readonly name: string) {}
}
export class Engineer extends Employee {
constructor(
name: string,
readonly level: number,
) {
super(name);
}
}
export class Salesman extends Employee {
constructor(
name: string,
readonly region: string,
readonly quota: number,
) {
super(name);
}
quotaMet(sales: number): boolean {
return sales >= this.quota;
}
}
const li = new Salesman("李娜", "华东", 500000);
console.log(li.name, li.quotaMet(600000), li.quotaMet(400000));
console.log(new Engineer("张伟", 3).name);
// 输出:
// 李娜 true false
// 张伟

下移后:quota 和 quota_met 都在 Salesman 里,Employee 只剩 name

Employee 的构造函数少了一个带默认值的参数,“0 是没有指标还是指标为零”这个问题不复存在:没有指标的员工根本没有这个字段。if e.quota == 0 这种靠默认值猜身份的写法也就写不出来了。

五段代码的输出和重构前逐字相同,五组测试一个字没改,在重构前后都通过。

字段上移

  1. 检查各子类里的这个字段是不是同一个东西:名字相同、含义相同、用法相同。名字不同但含义相同的,先用字段改名(第 31 章)统一名字。(IDE 可自动完成)
  2. 在父类构造函数里加上这个参数和赋值,子类构造函数改成把值传给 super,删掉子类自己的赋值。TS 里同时把字段声明搬到父类。
  3. 跑测试。然后把只接收“子类甲或子类乙”的函数参数类型放宽成父类。

构造函数本体上移

  1. 父类还没有构造函数的,先建一个,子类改成调用它。
  2. 把各子类构造函数里相同的语句搬进父类,需要的值作为参数传上去。跑测试。
  3. 如果相同的语句排在子类独有语句的后面,而且依赖子类数据,就不能直接上移:TS 里 super() 之前不能使用 this。这时先用提炼函数(第 4 章)把这段公共逻辑提成一个方法,再用函数上移把方法搬到父类,子类构造函数在合适的位置调用它。

函数上移

  1. 确认各子类的函数做的是同一件事。像 monthly_cost * 12 和 12 * monthly_cost 这样写法不同的,先改成一模一样,跑测试,确认它们真的等价。
  2. 检查函数体用到的字段和方法在父类里是否都有,没有的先上移它们。
  3. 把函数复制到父类,跑测试;再逐个删掉子类里的版本,每删一个跑一次测试。PyCharm 和 WebStorm 的 Pull Members Up 能完成搬移和删除(IDE 可自动完成)。

函数下移和字段下移

  1. 找出真正用到它的子类。用 IDE 的 Find Usages 查调用点,看调用者手里拿的是哪个子类。
  2. 把函数或字段复制到用到它的子类,跑测试。字段下移时,构造参数也跟着从父类挪到子类。
  3. 从父类删掉,跑测试。如果有调用者是通过父类类型调用的,测试或类型检查会指出来,说明它其实只该拿着那个子类。PyCharm 和 WebStorm 的 Push Members Down 能完成搬移(IDE 可自动完成)。

有函数和字段要一起下移时,先移函数后移字段:函数还在父类时,字段必须也在父类,否则 TS 编译不过。

上移:把几个子类里相同的函数、字段或构造函数语句搬到它们共同的父类。下移:把父类里只与某个子类相关的函数或字段搬到那个子类。

上移最常出现在“照着抄”的继承体系里:第一个子类写好了,第二个子类复制过来改几处,相同的部分就留下了双份。另一个常见来源是第 54 章提炼超类:刚提出父类时它是空的,接下来的工作就是一个个地上移。判断能不能上移,看的是“是不是同一件事”,而不是“代码长得像不像”。两个 annual_cost 写法不同却是同一件事,可以上移;两个都叫 price 的方法,一个含税一个不含税,长得再像也不能合并。

下移出现在父类被当成了“公共储物间”的时候:某个子类需要一个字段,加在父类最方便,于是就加在了父类。时间一长,父类变成所有子类的并集,每个子类都背着别人的东西。判断标准是看调用者:如果一个方法的所有调用者手里拿的都是同一个子类,它就该住在那个子类里。

上移和下移互为反向。两个方向不是谁对谁错,而是随需求移动:今天只有销售有提成,提成就该在销售里;明年经理也有提成了,就再把它上移,或者提炼出一个“有提成的员工”中间层。

函数上移经常停在一半:两个子类的函数大部分相同,只有一两步不同。比如销售的年成本要在十二个月工资之外再加年度提成,工程师的不用。这时上移的是整个流程,不同的那一步留在子类里作为一个可覆盖的方法:Employee.annual_cost() 返回 self.monthly_cost * 12 + self.annual_bonus(),annual_bonus() 在父类里默认返回 0,Salesman 覆盖它。这就是模板方法(Template Method)模式:父类的 annual_cost 是模板方法,定下步骤和顺序;annual_bonus 是钩子,留给子类填空。下面真实例子里 Django 的 ListView.get 和 get_queryset 也是同样的分工。模板方法见《设计模式》书第 20 章。

Django 的类视图(class-based views)就是函数上移的产物。早期项目里每个列表页都是一个视图函数,各写一遍“取第几页、切片、渲染”;Django 把这段公共流程上移到 ListView 里,子类只提供 get_queryset。下面用字典模拟请求,ArticleListV1、ProductListV1 是上移前的两个视图,V2 是上移后的。

"""模拟 Django 的类视图:把两个列表视图里一样的 get 上移到基类。"""
ARTICLES = ["重构入门", "测试先行", "代码坏味道"]
PRODUCTS = ["咖啡豆", "手冲壶", "滤纸", "磨豆机"]
PAGE_SIZE = 2
class View:
"""模拟 Django 的 View:按请求方法分发到同名方法。"""
def dispatch(self, request: dict) -> str:
return getattr(self, request["method"].lower())(request)
# 上移前:两个视图各写一遍分页和渲染。
class ArticleListV1(View):
def get(self, request: dict) -> str:
page = int(request.get("page", 1))
items = ARTICLES[(page - 1) * PAGE_SIZE : page * PAGE_SIZE]
return f"文章 第{page}页:{'、'.join(items)}"
class ProductListV1(View):
def get(self, request: dict) -> str:
page = int(request.get("page", 1))
items = PRODUCTS[(page - 1) * PAGE_SIZE : page * PAGE_SIZE]
return f"商品 第{page}页:{'、'.join(items)}"
# 上移后:get 住在 ListView 里,子类只说"列什么、叫什么"。
class ListView(View):
title = ""
def get_queryset(self) -> list[str]:
raise NotImplementedError
def get(self, request: dict) -> str:
page = int(request.get("page", 1))
items = self.get_queryset()[(page - 1) * PAGE_SIZE : page * PAGE_SIZE]
return f"{self.title} 第{page}页:{'、'.join(items)}"
class ArticleListV2(ListView):
title = "文章"
def get_queryset(self) -> list[str]:
return ARTICLES
class ProductListV2(ListView):
title = "商品"
def get_queryset(self) -> list[str]:
return PRODUCTS
if __name__ == "__main__":
request = {"method": "GET", "page": "2"}
print(ArticleListV1().dispatch(request) == ArticleListV2().dispatch(request))
print(ProductListV1().dispatch(request) == ProductListV2().dispatch(request))
print(ProductListV2().dispatch(request))
# 输出:
# True
# True
# 商品 第2页:滤纸、磨豆机

上移之后,要给所有列表页加“每页条数可配置”,改 ListView.get 一处即可。真正的 Django 走得更远:paginate_by、template_name 都是可以在子类里覆盖的类属性,分页、取数据、渲染模板各是一个可覆盖的方法。

前端项目里,登录表单和注册表单常常是两个组件,各写一遍“检查必填项、拼错误提示”。下面用普通的类模拟组件,render 返回要显示的文字。上移之后,校验逻辑住在 FormComponent 里,子类只声明标题和必填字段。

// 模拟前端表单组件:把两个表单里一样的必填校验上移到基类。
type Values = Record<string, string>;
// 上移前:两个组件各写一遍校验。
class LoginFormV1 {
readonly required = ["email", "password"];
validate(values: Values): string[] {
return this.required.filter((f) => !values[f]?.trim()).map((f) => `${f} 必填`);
}
render(values: Values): string {
const errors = this.validate(values);
return errors.length > 0 ? `登录表单:${errors.join(",")}` : "登录表单:可以提交";
}
}
class SignupFormV1 {
readonly required = ["email", "password", "nickname"];
validate(values: Values): string[] {
return this.required.filter((f) => !values[f]?.trim()).map((f) => `${f} 必填`);
}
render(values: Values): string {
const errors = this.validate(values);
return errors.length > 0 ? `注册表单:${errors.join(",")}` : "注册表单:可以提交";
}
}
// 上移后:校验和渲染住在基类里。
abstract class FormComponent {
abstract readonly title: string;
abstract readonly required: string[];
validate(values: Values): string[] {
return this.required.filter((f) => !values[f]?.trim()).map((f) => `${f} 必填`);
}
render(values: Values): string {
const errors = this.validate(values);
return errors.length > 0 ? `${this.title}:${errors.join(",")}` : `${this.title}:可以提交`;
}
}
class LoginFormV2 extends FormComponent {
readonly title = "登录表单";
readonly required = ["email", "password"];
}
class SignupFormV2 extends FormComponent {
readonly title = "注册表单";
readonly required = ["email", "password", "nickname"];
}
const values: Values = { email: "[email protected]", password: " " };
console.log(new LoginFormV1().render(values) === new LoginFormV2().render(values));
console.log(new SignupFormV1().render(values) === new SignupFormV2().render(values));
console.log(new SignupFormV2().render(values));
// 输出:
// true
// true
// 注册表单:password 必填,nickname 必填

上移之后,“只填空格也算没填”这条规则只写在一处。React 社区今天更常用的做法是组合:把校验写成一个函数或一个 hook,各组件调用它,而不是继承同一个基类。两种做法都消除了重复;组件之间除了校验还共享大量行为时,基类更顺手,只共享一两个工具函数时,组合更轻。第 56 章会讲从继承换到组合的手法。

上移过头的样子是父类越来越胖:只要两个子类有点像的代码就往上搬,最后父类里一半的方法只被两个子类中的一个用,又回到了需要下移的状态。上移之前问一句:加第三个子类时,它也会需要这个东西吗?如果答案是“不一定”,宁可先留着两份重复,等第三个子类出现时再看。

另一个陷阱是把“巧合相同”当成“本质相同”。工程师和销售的年假天数今天碰巧都是 10 天,上移成父类的一个常量之后,下个月人事说销售改成 12 天,就得再下移回去,或者在父类里加一个按类型分支的判断,这比原来的重复更糟。公共的应该是规则,而不是今天碰巧相等的数值。

下移也有代价:调用方如果手里拿的是父类类型,下移之后就得先判断类型再调用。如果下移之后到处冒出 isinstance(e, Salesman),说明这个成员其实是父类层面的概念,只是不同子类的实现不同,这时该在父类里留一个抽象声明,让各子类给出自己的实现,而不是下移。

手法 它关心什么 上移、下移关心什么
搬移函数、搬移字段(第 21、22 章) 成员在两个互不相干的类之间搬家 成员在同一继承体系的父子类之间上下移动
提炼超类(第 54 章) 两个还没有共同父类的类,新建一个父类 已经有父类,往上或往下挪成员;提炼超类之后通常紧接着上移
以委托取代超类(第 56 章) 子类根本不该继承这个父类,改成持有 继承关系没错,只是成员放错了层

下面的车辆类有两个问题:Car 和 Truck 的 label 一模一样,Vehicle 上的 cargo_kg 只有货车用。用函数上移把 label 搬到 Vehicle(不同的部分 summary 留在子类),用字段下移把 cargo_kg 搬到 Truck。

"""练习:车辆标签。"""
class Vehicle:
def __init__(self, plate: str, cargo_kg: int = 0) -> None:
self.plate = plate
self.cargo_kg = cargo_kg
class Car(Vehicle):
def __init__(self, plate: str, seats: int) -> None:
super().__init__(plate)
self.seats = seats
def summary(self) -> str:
return f"{self.seats} 座轿车"
def label(self) -> str:
return f"[{self.plate}] {self.summary()}"
class Truck(Vehicle):
def __init__(self, plate: str, cargo_kg: int) -> None:
super().__init__(plate, cargo_kg)
def summary(self) -> str:
return f"载重 {self.cargo_kg} 公斤货车"
def label(self) -> str:
return f"[{self.plate}] {self.summary()}"
if __name__ == "__main__":
print(Car("沪A12345", 5).label())
print(Truck("沪B67890", 3000).label())
# 输出:
# [沪A12345] 5 座轿车
# [沪B67890] 载重 3000 公斤货车
import unittest
from target import Car, Truck
class VehicleTest(unittest.TestCase):
def test_car_label(self):
self.assertEqual(Car("沪A12345", 5).label(), "[沪A12345] 5 座轿车")
def test_truck_label(self):
self.assertEqual(Truck("沪B67890", 3000).label(), "[沪B67890] 载重 3000 公斤货车")
def test_truck_keeps_cargo(self):
self.assertEqual(Truck("沪B67890", 3000).cargo_kg, 3000)
if __name__ == "__main__":
unittest.main()
// 练习:车辆标签。
export class Vehicle {
constructor(
readonly plate: string,
readonly cargoKg: number = 0,
) {}
}
export class Car extends Vehicle {
constructor(
plate: string,
readonly seats: number,
) {
super(plate);
}
summary(): string {
return `${this.seats} 座轿车`;
}
label(): string {
return `[${this.plate}] ${this.summary()}`;
}
}
export class Truck extends Vehicle {
constructor(plate: string, cargoKg: number) {
super(plate, cargoKg);
}
summary(): string {
return `载重 ${this.cargoKg} 公斤货车`;
}
label(): string {
return `[${this.plate}] ${this.summary()}`;
}
}
console.log(new Car("沪A12345", 5).label());
console.log(new Truck("沪B67890", 3000).label());
// 输出:
// [沪A12345] 5 座轿车
// [沪B67890] 载重 3000 公斤货车
import test from "node:test";
import assert from "node:assert/strict";
import { Car, Truck } from "./target";
test("轿车标签", () => {
assert.equal(new Car("沪A12345", 5).label(), "[沪A12345] 5 座轿车");
});
test("货车标签", () => {
assert.equal(new Truck("沪B67890", 3000).label(), "[沪B67890] 载重 3000 公斤货车");
});
test("货车记得载重", () => {
assert.equal(new Truck("沪B67890", 3000).cargoKg, 3000);
});

提示:label 上移后要调用 summary,父类得知道每个子类都有 summary。Python 里可以让 Vehicle 继承 ABC 并把 summary 声明为抽象方法,TS 里用 abstract summary(): string。

  • 上移消除子类之间的重复:字段、构造函数语句、函数,通常按这个顺序做,前一步给后一步铺路。
  • 能不能上移看“是不是同一件事”,不看代码长得像不像;写法不同的先改成一样,测试确认等价后再上移。
  • 下移清理父类替某个子类背着的东西:所有调用者拿的都是同一个子类,这个成员就该住在那个子类里。
  • 大部分相同、一两步不同的函数,上移流程、留下钩子,就是模板方法。
  • 上移和下移随需求来回移动,别为今天碰巧相等的东西上移。