跳转到内容

以子类取代类型码

以子类取代类型码(Replace Type Code with Subclasses)把一个表示“是哪一种”的字段,比如 type = "engineer",换成一组子类,每个取值一个子类,按类型分支的逻辑搬进各自的子类。 它解决的痛是:同一个类型字段在好几个方法里被 match、被 if 判断,每加一种类型就要把这些分支挨个找出来改一遍,漏掉一个就悄悄走进默认分支。

小周负责公司薪酬系统里的员工模块。员工按类型分工程师、经理、销售,数据库里存的是一个字符串 type。显示头衔要看类型,算年终奖要看类型,构造时校验也要看类型。年底人事说明年要招实习生,实习生的头衔和奖金规则都不一样,让小周先评估一下要改多少地方。

团队早就约定通过 create_employee 创建员工,不直接调用构造函数(第 48 章以工厂函数取代构造函数),所以下面的代码和测试都从它入手。

"""以子类取代类型码:重构前。"""
TYPES = ("engineer", "manager", "salesman")
class Employee:
def __init__(self, name: str, type_: str) -> None:
if type_ not in TYPES:
raise ValueError(f"未知的员工类型:{type_}")
self.name = name
self.type = type_
def title(self) -> str:
match self.type:
case "engineer":
return "工程师"
case "manager":
return "经理"
case _:
return "销售"
def bonus(self, monthly_salary: int) -> int:
match self.type:
case "engineer":
return monthly_salary
case "manager":
return monthly_salary * 3
case _:
return monthly_salary * 2
def __str__(self) -> str:
return f"{self.name}({self.title()})"
def create_employee(name: str, type_: str) -> Employee:
return Employee(name, type_)
if __name__ == "__main__":
for name, type_ in [("张伟", "engineer"), ("王芳", "manager"), ("李娜", "salesman")]:
e = create_employee(name, type_)
print(e, "年终奖", e.bonus(20000))
try:
create_employee("赵敏", "intern")
except ValueError as error:
print(error)
# 输出:
# 张伟(工程师) 年终奖 20000
# 王芳(经理) 年终奖 60000
# 李娜(销售) 年终奖 40000
# 未知的员工类型:intern
// 以子类取代类型码:重构前。
const TYPES = ["engineer", "manager", "salesman"];
export class Employee {
constructor(
readonly name: string,
readonly type: string,
) {
if (!TYPES.includes(type)) throw new Error(`未知的员工类型:${type}`);
}
title(): string {
switch (this.type) {
case "engineer":
return "工程师";
case "manager":
return "经理";
default:
return "销售";
}
}
bonus(monthlySalary: number): number {
switch (this.type) {
case "engineer":
return monthlySalary;
case "manager":
return monthlySalary * 3;
default:
return monthlySalary * 2;
}
}
toString(): string {
return `${this.name}(${this.title()})`;
}
}
export function createEmployee(name: string, type: string): Employee {
return new Employee(name, type);
}
const people: [string, string][] = [
["张伟", "engineer"],
["王芳", "manager"],
["李娜", "salesman"],
];
for (const [name, type] of people) {
const e = createEmployee(name, type);
console.log(`${e}`, "年终奖", e.bonus(20000));
}
try {
createEmployee("赵敏", "intern");
} catch (error) {
if (error instanceof Error) console.log(error.message);
}
// 输出:
// 张伟(工程师) 年终奖 20000
// 王芳(经理) 年终奖 60000
// 李娜(销售) 年终奖 40000
// 未知的员工类型:intern

TS 版的 type 用的是 string 而不是字面量联合类型,因为它和 Python 版一样来自数据库,构造时才校验。测试覆盖头衔、奖金、类型字段和非法类型:

import unittest
from target import create_employee
class EmployeeTest(unittest.TestCase):
def test_title(self):
self.assertEqual(str(create_employee("张伟", "engineer")), "张伟(工程师)")
self.assertEqual(create_employee("李娜", "salesman").title(), "销售")
def test_bonus_by_type(self):
self.assertEqual(create_employee("张伟", "engineer").bonus(20000), 20000)
self.assertEqual(create_employee("王芳", "manager").bonus(20000), 60000)
self.assertEqual(create_employee("李娜", "salesman").bonus(20000), 40000)
def test_type_is_still_readable(self):
self.assertEqual(create_employee("王芳", "manager").type, "manager")
def test_unknown_type_is_rejected(self):
with self.assertRaisesRegex(ValueError, "未知的员工类型:intern"):
create_employee("赵敏", "intern")
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { createEmployee } from "./target";
test("头衔", () => {
assert.equal(`${createEmployee("张伟", "engineer")}`, "张伟(工程师)");
assert.equal(createEmployee("李娜", "salesman").title(), "销售");
});
test("按类型算年终奖", () => {
assert.equal(createEmployee("张伟", "engineer").bonus(20000), 20000);
assert.equal(createEmployee("王芳", "manager").bonus(20000), 60000);
assert.equal(createEmployee("李娜", "salesman").bonus(20000), 40000);
});
test("类型字段照样能读", () => {
assert.equal(createEmployee("王芳", "manager").type, "manager");
});
test("拒绝未知类型", () => {
assert.throws(() => createEmployee("赵敏", "intern"), /未知的员工类型:intern/);
});

如果现在要加实习生 intern,小周要改三处:TYPES 里加一项,title 里加一个分支,bonus 里加一个分支。这还只是这一个类里的;系统里还有考勤、报销审批、权限几个模块也在读 employee.type,各自写着自己的 if e.type == "manager",得全局搜索字符串 "manager" 才能找全。

漏改的后果是安静的。假设他记得改 TYPES 和 bonus,忘了 title,实习生的头衔会显示成“销售”,因为 title 的最后一个分支是 case _。没有报错,没有测试失败,直到有人在工牌上看到“赵敏(销售)”。默认分支在写的时候是为了少写一行,加类型的时候它变成了一个陷阱。

读的时候也累。想知道“经理的全部规则”,要在 title 里找经理那一行,再到 bonus 里找经理那一行,再去别的模块里搜。关于经理的知识按方法切碎了,散在各处;而每个方法又按类型切成了三段,读一个方法要在脑子里同时装下三种员工。

用第 2 章的名字说,这是重复的 switch:同样按 type 分支的结构出现在多个方法里;而用一个裸字符串表示“员工是哪一种”,是基本类型偏执。

重构前:Employee 用一个字符串字段 type 区分三种员工,title 和 bonus 各自按它分支

"""以子类取代类型码:重构后。"""
from abc import ABC, abstractmethod
from typing import ClassVar
class Employee(ABC):
type: ClassVar[str]
def __init__(self, name: str) -> None:
self.name = name
@abstractmethod
def title(self) -> str: ...
@abstractmethod
def bonus(self, monthly_salary: int) -> int: ...
def __str__(self) -> str:
return f"{self.name}({self.title()})"
class Engineer(Employee):
type = "engineer"
def title(self) -> str:
return "工程师"
def bonus(self, monthly_salary: int) -> int:
return monthly_salary
class Manager(Employee):
type = "manager"
def title(self) -> str:
return "经理"
def bonus(self, monthly_salary: int) -> int:
return monthly_salary * 3
class Salesman(Employee):
type = "salesman"
def title(self) -> str:
return "销售"
def bonus(self, monthly_salary: int) -> int:
return monthly_salary * 2
def create_employee(name: str, type_: str) -> Employee:
match type_:
case "engineer":
return Engineer(name)
case "manager":
return Manager(name)
case "salesman":
return Salesman(name)
case _:
raise ValueError(f"未知的员工类型:{type_}")
if __name__ == "__main__":
for name, type_ in [("张伟", "engineer"), ("王芳", "manager"), ("李娜", "salesman")]:
e = create_employee(name, type_)
print(e, "年终奖", e.bonus(20000))
try:
create_employee("赵敏", "intern")
except ValueError as error:
print(error)
# 输出:
# 张伟(工程师) 年终奖 20000
# 王芳(经理) 年终奖 60000
# 李娜(销售) 年终奖 40000
# 未知的员工类型:intern
// 以子类取代类型码:重构后。
export abstract class Employee {
abstract readonly type: string;
constructor(readonly name: string) {}
abstract title(): string;
abstract bonus(monthlySalary: number): number;
toString(): string {
return `${this.name}(${this.title()})`;
}
}
export class Engineer extends Employee {
readonly type = "engineer";
title(): string {
return "工程师";
}
bonus(monthlySalary: number): number {
return monthlySalary;
}
}
export class Manager extends Employee {
readonly type = "manager";
title(): string {
return "经理";
}
bonus(monthlySalary: number): number {
return monthlySalary * 3;
}
}
export class Salesman extends Employee {
readonly type = "salesman";
title(): string {
return "销售";
}
bonus(monthlySalary: number): number {
return monthlySalary * 2;
}
}
export function createEmployee(name: string, type: string): Employee {
switch (type) {
case "engineer":
return new Engineer(name);
case "manager":
return new Manager(name);
case "salesman":
return new Salesman(name);
default:
throw new Error(`未知的员工类型:${type}`);
}
}
const people: [string, string][] = [
["张伟", "engineer"],
["王芳", "manager"],
["李娜", "salesman"],
];
for (const [name, type] of people) {
const e = createEmployee(name, type);
console.log(`${e}`, "年终奖", e.bonus(20000));
}
try {
createEmployee("赵敏", "intern");
} catch (error) {
if (error instanceof Error) console.log(error.message);
}
// 输出:
// 张伟(工程师) 年终奖 20000
// 王芳(经理) 年终奖 60000
// 李娜(销售) 年终奖 40000
// 未知的员工类型:intern

重构后:每种员工一个子类,各自实现 title 和 bonus;type 变成子类上的常量

输出一字不差,四个测试照常通过。再看上一节的几个问题。

加实习生现在是加一个类 Intern,再在 create_employee 里加一个分支。Employee 把 title 和 bonus 声明成了抽象方法,Intern 少写哪一个,Python 在实例化时就抛 TypeError,TS 在编译时就报错。“忘了改 title、头衔悄悄显示成销售”这种事不会再发生,因为已经没有那个默认分支了。

全系统里按类型分支的 match 只剩一个,就在工厂函数里。这一个是躲不掉的:数据库里存的是字符串,总得有一个地方把字符串翻译成类。但它只负责“创建哪个类”,不负责任何业务规则,加类型时改它不需要理解奖金怎么算。

想知道经理的全部规则,打开 Manager 就行,头衔和奖金挨在一起。别的模块如果也在按 type 判断,可以照同样的办法把那些逻辑搬进子类,最终 type 只剩下“存数据库时写什么”这一个用途。

type 字段保留了下来,变成每个子类上的常量。这是为了不打断还在读 employee.type 的代码,比如保存到数据库的那一段。

Fowler 的做法分两段:先让子类出现,再把条件逻辑搬进去。每一步之后都跑测试。

  1. 自封装类型码:如果外部代码直接读写 type 字段,先用封装变量(第 8 章)让它只通过一个取值函数访问。本例里 type 只在构造时设置、之后只读,可以跳过。
  2. 确保只有一个创建入口。如果代码里到处直接调用 Employee(name, type),先用以工厂函数取代构造函数(第 48 章)把它们收进 create_employee。这一步是关键:之后要按类型返回不同的子类,只有工厂函数做得到,构造函数做不到。
  3. 挑一个类型值,比如 "engineer",建子类 Engineer,让它覆盖类型码的取值,返回字面量 "engineer"。在工厂函数里加一个分支:类型是 "engineer" 时返回 Engineer。跑测试。
  4. 对 "manager"、"salesman" 重复第 3 步,每加一个跑一次测试。
  5. 工厂函数的最后一个分支改成抛出“未知类型”错误,构造函数里的校验和 TYPES 可以删了。父类里的 type 字段也删掉,改由各子类提供。跑测试。
  6. 对每个按类型分支的方法,用以多态取代条件表达式(第 38 章)把各分支搬进对应子类,父类里留下抽象声明。从 title 开始,搬一个分支跑一次测试。只和某个子类相关的方法用函数下移(第 51 章)处理。

第 3 到第 5 步做完时,子类已经存在,但 title 和 bonus 里的 match 还在,还是按 self.type 分支,只不过 type 的值改由子类提供。这是一个可以停下来提交的中间状态,测试全部通过。第 6 步才真正收获好处,这一步和第 38 章是同一件事,本章不展开。

IDE 能帮上的是机械部分:建好子类之后,PyCharm 和 WebStorm 的 Push Members Down 可以把方法复制到子类(IDE 可自动完成)。“哪个分支属于哪个子类”的判断要自己做。

以子类取代类型码:用一组子类代替一个表示对象种类的字段,每个类型值对应一个子类。

它常在两种场合出现。一是本章这样:同一个类型字段在多个方法里驱动条件分支,子类让每种类型的行为住在一起,再加类型就是加一个类。二是某些字段或方法只对某一种类型有意义,比如只有销售有业绩指标。有了子类之后,这些成员可以下移到 Salesman,工程师对象上就不会再挂着一个永远是 0 的 quota,这正是第 51 章字段下移的前提。

有时直接子类化做不到。最常见的原因是类型会变:工程师升职成经理,对象还是同一个对象,可子类在创建时就定下了,Python 和 TS 都不该在运行时改一个对象的类。另一个原因是这个类已经按别的维度有了子类,比如员工已经分了全职和兼职,再按岗位分就要组合出六个子类。这两种情况用间接继承:不给 Employee 建子类,而是给“员工类型”建一个类层次,Employee 持有一个类型对象,类型变了就换一个类型对象。

"""以子类取代类型码(间接继承):重构前。"""
TYPES = ("engineer", "manager", "salesman")
def check_type(type_: str) -> str:
if type_ not in TYPES:
raise ValueError(f"未知的员工类型:{type_}")
return type_
class Employee:
def __init__(self, name: str, type_: str) -> None:
self.name = name
self.type = check_type(type_)
def promote(self, new_type: str) -> None:
self.type = check_type(new_type)
def bonus(self, monthly_salary: int) -> int:
match self.type:
case "engineer":
return monthly_salary
case "manager":
return monthly_salary * 3
case _:
return monthly_salary * 2
if __name__ == "__main__":
zhang = Employee("张伟", "engineer")
print(zhang.type, zhang.bonus(20000))
zhang.promote("manager")
print(zhang.type, zhang.bonus(20000))
# 输出:
# engineer 20000
# manager 60000
"""以子类取代类型码(间接继承):重构后。"""
from abc import ABC, abstractmethod
from typing import ClassVar
class EmployeeType(ABC):
code: ClassVar[str]
@abstractmethod
def bonus(self, monthly_salary: int) -> int: ...
class EngineerType(EmployeeType):
code = "engineer"
def bonus(self, monthly_salary: int) -> int:
return monthly_salary
class ManagerType(EmployeeType):
code = "manager"
def bonus(self, monthly_salary: int) -> int:
return monthly_salary * 3
class SalesmanType(EmployeeType):
code = "salesman"
def bonus(self, monthly_salary: int) -> int:
return monthly_salary * 2
def create_employee_type(code: str) -> EmployeeType:
match code:
case "engineer":
return EngineerType()
case "manager":
return ManagerType()
case "salesman":
return SalesmanType()
case _:
raise ValueError(f"未知的员工类型:{code}")
class Employee:
def __init__(self, name: str, type_: str) -> None:
self.name = name
self._type = create_employee_type(type_)
@property
def type(self) -> str:
return self._type.code
def promote(self, new_type: str) -> None:
self._type = create_employee_type(new_type)
def bonus(self, monthly_salary: int) -> int:
return self._type.bonus(monthly_salary)
if __name__ == "__main__":
zhang = Employee("张伟", "engineer")
print(zhang.type, zhang.bonus(20000))
zhang.promote("manager")
print(zhang.type, zhang.bonus(20000))
# 输出:
# engineer 20000
# manager 60000
// 以子类取代类型码(间接继承):重构前。
const TYPES = ["engineer", "manager", "salesman"];
function checkType(type: string): string {
if (!TYPES.includes(type)) throw new Error(`未知的员工类型:${type}`);
return type;
}
export class Employee {
type: string;
constructor(
readonly name: string,
type: string,
) {
this.type = checkType(type);
}
promote(newType: string): void {
this.type = checkType(newType);
}
bonus(monthlySalary: number): number {
switch (this.type) {
case "engineer":
return monthlySalary;
case "manager":
return monthlySalary * 3;
default:
return monthlySalary * 2;
}
}
}
const zhang = new Employee("张伟", "engineer");
console.log(zhang.type, zhang.bonus(20000));
zhang.promote("manager");
console.log(zhang.type, zhang.bonus(20000));
// 输出:
// engineer 20000
// manager 60000
// 以子类取代类型码(间接继承):重构后。
abstract class EmployeeType {
abstract readonly code: string;
abstract bonus(monthlySalary: number): number;
}
class EngineerType extends EmployeeType {
readonly code = "engineer";
bonus(monthlySalary: number): number {
return monthlySalary;
}
}
class ManagerType extends EmployeeType {
readonly code = "manager";
bonus(monthlySalary: number): number {
return monthlySalary * 3;
}
}
class SalesmanType extends EmployeeType {
readonly code = "salesman";
bonus(monthlySalary: number): number {
return monthlySalary * 2;
}
}
function createEmployeeType(code: string): EmployeeType {
switch (code) {
case "engineer":
return new EngineerType();
case "manager":
return new ManagerType();
case "salesman":
return new SalesmanType();
default:
throw new Error(`未知的员工类型:${code}`);
}
}
export class Employee {
private employeeType: EmployeeType;
constructor(
readonly name: string,
type: string,
) {
this.employeeType = createEmployeeType(type);
}
get type(): string {
return this.employeeType.code;
}
promote(newType: string): void {
this.employeeType = createEmployeeType(newType);
}
bonus(monthlySalary: number): number {
return this.employeeType.bonus(monthlySalary);
}
}
const zhang = new Employee("张伟", "engineer");
console.log(zhang.type, zhang.bonus(20000));
zhang.promote("manager");
console.log(zhang.type, zhang.bonus(20000));
// 输出:
// engineer 20000
// manager 60000
import unittest
from target import Employee
class PromoteTest(unittest.TestCase):
def test_bonus_follows_current_type(self):
zhang = Employee("张伟", "engineer")
self.assertEqual(zhang.bonus(20000), 20000)
zhang.promote("manager")
self.assertEqual((zhang.type, zhang.bonus(20000)), ("manager", 60000))
def test_unknown_type_is_rejected(self):
with self.assertRaisesRegex(ValueError, "未知的员工类型:intern"):
Employee("张伟", "engineer").promote("intern")
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { Employee } from "./target";
test("奖金跟着当前类型走", () => {
const zhang = new Employee("张伟", "engineer");
assert.equal(zhang.bonus(20000), 20000);
zhang.promote("manager");
assert.deepEqual([zhang.type, zhang.bonus(20000)], ["manager", 60000]);
});
test("拒绝未知类型", () => {
assert.throws(() => new Employee("张伟", "engineer").promote("intern"), /未知的员工类型:intern/);
});

间接继承:Employee 持有一个 EmployeeType,promote 换掉这个对象,三种类型各是 EmployeeType 的子类

Employee 本身没有子类,升职时它还是同一个对象,名字、入职记录都在;变的只是它持有的那个 EmployeeType。按类型分支的逻辑同样住进了子类,只不过是类型对象的子类。

间接继承的结果就是状态(State)模式:Employee 是环境(Context),EmployeeType 是状态接口,EngineerType、ManagerType、SalesmanType 是具体状态,promote 就是状态切换。行为随状态变化,而对象的身份不变。状态模式见《设计模式》书第 21 章。

如果类型对象不是随业务流程切换,而是从外部选定、注入进来的一种算法,比如奖金规则由配置决定,同样的结构就叫策略(Strategy)模式,见《设计模式》书第 17 章。两者的代码形状几乎一样,区别在意图:状态模式里对象会自己从一个状态走到另一个状态,策略模式里策略通常由使用者挑好了交进来。直接子类化的版本不对应某个特定模式,它就是第 38 章以多态取代条件表达式的前置步骤。

后台系统发通知,渠道有邮件、短信、站内信,常见的写法是一个 Notifier 带一个 channel 字段,发送、计费、长度限制都按它分支。下面把它换成每个渠道一个子类,notifier_for 是唯一按渠道分支的地方,用列表模拟发出去的消息。

"""通知渠道:用子类取代 channel 字段。"""
from abc import ABC, abstractmethod
OUTBOX: list[str] = []
class NotifierV1:
"""重构前:一个类,按 channel 分支。"""
def __init__(self, channel: str) -> None:
self.channel = channel
def send(self, to: str, text: str) -> None:
if self.channel == "sms":
text = text[:7]
prefix = {"email": "邮件", "sms": "短信", "inbox": "站内信"}[self.channel]
OUTBOX.append(f"{prefix} -> {to}:{text}")
class Notifier(ABC):
"""重构后:每个渠道一个子类。"""
label = ""
def send(self, to: str, text: str) -> None:
OUTBOX.append(f"{self.label} -> {to}:{self.shape(text)}")
@abstractmethod
def shape(self, text: str) -> str: ...
class EmailNotifier(Notifier):
label = "邮件"
def shape(self, text: str) -> str:
return text
class SmsNotifier(Notifier):
label = "短信"
def shape(self, text: str) -> str:
return text[:7]
class InboxNotifier(Notifier):
label = "站内信"
def shape(self, text: str) -> str:
return text
def notifier_for(channel: str) -> Notifier:
return {"email": EmailNotifier, "sms": SmsNotifier, "inbox": InboxNotifier}[channel]()
if __name__ == "__main__":
text = "您的订单已发货,请注意查收"
for channel in ("email", "sms", "inbox"):
NotifierV1(channel).send("小周", text)
before = list(OUTBOX)
OUTBOX.clear()
for channel in ("email", "sms", "inbox"):
notifier_for(channel).send("小周", text)
print(before == OUTBOX)
print(OUTBOX[1])
# 输出:
# True
# 短信 -> 小周:您的订单已发货

短信的截断规则现在只在 SmsNotifier 里。要加企业微信渠道,写一个 WeComNotifier,在 notifier_for 的字典里加一项,不用碰其他渠道的代码。Django 的邮件后端、日志模块的各种 Handler 都是这样按“种类”拆成子类的。

前端画图的代码里,图形常常是带 kind 字段的对象字面量,面积、描述各写一个 switch。下面把它们换成类。

// 图形:用子类取代 kind 字段。
// 重构前:对象字面量加 switch。
interface ShapeData {
kind: string;
size: number;
}
function areaV1(s: ShapeData): number {
switch (s.kind) {
case "circle":
return Math.PI * s.size * s.size;
case "square":
return s.size * s.size;
default:
throw new Error(`未知图形:${s.kind}`);
}
}
function describeV1(s: ShapeData): string {
return `${s.kind === "circle" ? "圆" : "正方形"} 面积 ${areaV1(s).toFixed(2)}`;
}
// 重构后:每种图形一个类。
abstract class Shape {
constructor(readonly size: number) {}
abstract readonly name: string;
abstract area(): number;
describe(): string {
return `${this.name} 面积 ${this.area().toFixed(2)}`;
}
}
class Circle extends Shape {
readonly name = "圆";
area(): number {
return Math.PI * this.size * this.size;
}
}
class Square extends Shape {
readonly name = "正方形";
area(): number {
return this.size * this.size;
}
}
function shapeFrom(data: ShapeData): Shape {
switch (data.kind) {
case "circle":
return new Circle(data.size);
case "square":
return new Square(data.size);
default:
throw new Error(`未知图形:${data.kind}`);
}
}
const data: ShapeData[] = [
{ kind: "circle", size: 1 },
{ kind: "square", size: 3 },
];
console.log(data.every((d) => describeV1(d) === shapeFrom(d).describe()));
for (const d of data) console.log(shapeFrom(d).describe());
// 输出:
// true
// 圆 面积 3.14
// 正方形 面积 9.00

TS 里还有一条路:把 kind 写成字面量联合类型(可辨识联合,discriminated union),switch 里漏写一种情况时编译器会报错。图形种类固定、操作经常增加时,可辨识联合更合适,加一个操作就是加一个函数;种类经常增加、操作相对固定时,子类更合适,加一个种类就是加一个类。这是同一个取舍的两面。

类型字段只在一两处被判断、而且分支里只有一行代码时,不值得建一组子类。一个 {"engineer": "工程师", ...} 字典比三个各含一行代码的类更容易读,也更容易改。判断标准是重复:同一组分支出现在几个地方?每个分支里有多少逻辑?两处以上、每处不止一行,子类才开始划算。

类型会变、或者已经按别的维度有了子类时,不要硬上直接子类化。为了支持升职去写“复制旧对象的所有字段、新建一个 Manager、再把所有引用换过去”的代码,比原来的 match 危险得多,用前面讲的间接继承。

第三种情况是类型的种类由运营在后台配置,今天三种明天五种,而每种的差别只是几个参数(奖金倍数、头衔文字)。这时差别是数据而不是行为,该做的是把参数存进一张配置表,用一个类读表,而不是每加一种类型就发一次版。

手法 它关心什么 以子类取代类型码关心什么
以多态取代条件表达式(第 38 章) 把已有的条件分支搬进子类 先让子类出现;通常紧接着做第 38 章
移除子类(第 53 章) 子类已经没什么差别,换回字段 反向手法:类型字段驱动了太多分支,换成子类
以对象取代基本类型(第 15 章) 给一个值加上校验和行为,变成值对象 让整个对象的行为随类型不同;间接继承时两者会合流

下面的票务代码按 kind 分支算票价和显示票种。用以子类取代类型码把它改成 AdultTicket、ChildTicket、SeniorTicket 三个子类,create_ticket 是唯一按 kind 分支的地方。

"""练习:票种。"""
class Ticket:
def __init__(self, kind: str) -> None:
self.kind = kind
def price(self, base: int) -> float:
if self.kind == "child":
return base * 50 / 100
if self.kind == "senior":
return base * 60 / 100
return float(base)
def label(self) -> str:
if self.kind == "child":
return "儿童票"
if self.kind == "senior":
return "长者票"
return "成人票"
def create_ticket(kind: str) -> Ticket:
return Ticket(kind)
if __name__ == "__main__":
for kind in ("adult", "child", "senior"):
ticket = create_ticket(kind)
print(ticket.label(), f"{ticket.price(80):.2f}")
# 输出:
# 成人票 80.00
# 儿童票 40.00
# 长者票 48.00
import unittest
from target import create_ticket
class TicketTest(unittest.TestCase):
def test_labels(self):
labels = [create_ticket(k).label() for k in ("adult", "child", "senior")]
self.assertEqual(labels, ["成人票", "儿童票", "长者票"])
def test_prices(self):
prices = [create_ticket(k).price(80) for k in ("adult", "child", "senior")]
self.assertEqual(prices, [80, 40, 48])
def test_kind_is_readable(self):
self.assertEqual(create_ticket("child").kind, "child")
if __name__ == "__main__":
unittest.main()
// 练习:票种。
export class Ticket {
constructor(readonly kind: string) {}
price(base: number): number {
if (this.kind === "child") return (base * 50) / 100;
if (this.kind === "senior") return (base * 60) / 100;
return base;
}
label(): string {
if (this.kind === "child") return "儿童票";
if (this.kind === "senior") return "长者票";
return "成人票";
}
}
export function createTicket(kind: string): Ticket {
return new Ticket(kind);
}
for (const kind of ["adult", "child", "senior"]) {
const ticket = createTicket(kind);
console.log(ticket.label(), ticket.price(80).toFixed(2));
}
// 输出:
// 成人票 80.00
// 儿童票 40.00
// 长者票 48.00
import test from "node:test";
import assert from "node:assert/strict";
import { createTicket } from "./target";
const KINDS = ["adult", "child", "senior"];
test("票种名称", () => {
assert.deepEqual(KINDS.map((k) => createTicket(k).label()), ["成人票", "儿童票", "长者票"]);
});
test("票价", () => {
assert.deepEqual(KINDS.map((k) => createTicket(k).price(80)), [80, 40, 48]);
});
test("票种字段照样能读", () => {
assert.equal(createTicket("child").kind, "child");
});
  • 同一个类型字段在多个方法里驱动分支,就是以子类取代类型码的信号;每种类型一个子类,该类型的规则住在一起。
  • 先用工厂函数收拢创建入口,因为只有工厂函数能按类型返回不同的子类;最后全系统只剩工厂里一个分支。
  • 去掉默认分支、在父类声明抽象方法,“加了类型忘了改某处”就从静默出错变成实例化或编译时报错。
  • 类型会变、或者已经按别的维度分过子类时,用间接继承:给类型建类层次,对象持有类型对象,这就是状态模式。
  • 分支少、每支一行、或差别只是配置数据时,别建子类。