以工厂函数取代构造函数
以工厂函数取代构造函数(Replace Constructor with Factory Function)用一个有名字的普通函数来创建对象,调用方不再直接调用构造函数。 它解决的痛是:构造函数的名字固定是类名,说不出“创建的是哪一种”,只能靠参数里的类型码暗示;它也只能返回这个类本身的新实例,不能返回子类,也不能复用已有对象。
人事系统里,员工用 Employee(name, type_code) 创建,类型码 "E" 是工程师,"M" 是经理,"S" 是销售。新员工入职时,类型来自入职表单;而项目组创建技术负责人时,代码里直接写死了 Employee("老周", "E")。
"""员工创建:重构前。"""
class Employee: TYPE_NAMES = {"E": "工程师", "M": "经理", "S": "销售"}
def __init__(self, name: str, type_code: str) -> None: self.name = name self.type_code = type_code
@property def type_name(self) -> str: return self.TYPE_NAMES[self.type_code]
if __name__ == "__main__": doc = {"name": "小孙", "emp_type": "S"} candidate = Employee(doc["name"], doc["emp_type"]) lead_engineer = Employee("老周", "E") for employee in (candidate, lead_engineer): print(employee.name, employee.type_name)
# 输出:# 小孙 销售# 老周 工程师// 员工创建:重构前。const TYPE_NAMES: Record<string, string> = { E: "工程师", M: "经理", S: "销售" };
export class Employee { constructor( readonly name: string, readonly typeCode: string, ) {}
get typeName(): string { return TYPE_NAMES[this.typeCode]; }}
const doc = { name: "小孙", empType: "S" };const candidate = new Employee(doc.name, doc.empType);const leadEngineer = new Employee("老周", "E");for (const employee of [candidate, leadEngineer]) { console.log(employee.name, employee.typeName);}
// 输出:// 小孙 销售// 老周 工程师如果现在要加一个需求:工程师要多一个“技术职级”字段和一套自己的考核规则,打算做成 Employee 的子类 Engineer。可是所有创建员工的地方写的都是 Employee(...),构造函数只能返回 Employee 本身,没法根据类型码返回子类。于是每个调用方都得改成自己判断:类型码是 "E" 就 Engineer(...),否则 Employee(...),这段判断会散落在入职、导入、测试数据等每个创建员工的地方。
Employee("老周", "E") 本身也难读。"E" 是什么,要去类里查类型码表。写错成 "e" 或 "EN",构造时不会报错,要等到读 type_name 时才抛 KeyError,离出错的那一行已经很远。
用第 2 章的名字说,这是基本类型偏执:一个字符串类型码承担了“员工是哪一种”这个概念,而构造函数让调用方不得不直接面对它。
"""员工创建:重构后。"""
class Employee: TYPE_NAMES = {"E": "工程师", "M": "经理", "S": "销售"}
def __init__(self, name: str, type_code: str) -> None: self.name = name self.type_code = type_code
@property def type_name(self) -> str: return self.TYPE_NAMES[self.type_code]
def create_employee(name: str, type_code: str) -> Employee: return Employee(name, type_code)
def create_engineer(name: str) -> Employee: return Employee(name, "E")
if __name__ == "__main__": doc = {"name": "小孙", "emp_type": "S"} candidate = create_employee(doc["name"], doc["emp_type"]) lead_engineer = create_engineer("老周") for employee in (candidate, lead_engineer): print(employee.name, employee.type_name)
# 输出:# 小孙 销售# 老周 工程师// 员工创建:重构后。const TYPE_NAMES: Record<string, string> = { E: "工程师", M: "经理", S: "销售" };
export class Employee { constructor( readonly name: string, readonly typeCode: string, ) {}
get typeName(): string { return TYPE_NAMES[this.typeCode]; }}
export function createEmployee(name: string, typeCode: string): Employee { return new Employee(name, typeCode);}
export function createEngineer(name: string): Employee { return new Employee(name, "E");}
const doc = { name: "小孙", empType: "S" };const candidate = createEmployee(doc.name, doc.empType);const leadEngineer = createEngineer("老周");for (const employee of [candidate, leadEngineer]) { console.log(employee.name, employee.typeName);}
// 输出:// 小孙 销售// 老周 工程师看上去只是多包了一层,好处在于以后。Engineer 子类来了,只改 create_employee 和 create_engineer 这两个函数的函数体,让它们在类型码是 "E" 时返回 Engineer,所有调用方一行不动。这就是第 52 章以子类取代类型码的第一步。类型码的校验也有了地方放:create_employee 里先查类型码表,写错的 "EN" 在创建时就报错。
create_engineer("老周") 不需要解释,类型码 "E" 从调用方消失了。工厂函数还能做构造函数做不到的另外两件事:返回已有对象(比如同一个员工编号只创建一次,和第 33 章将值对象改为引用对象里从仓库按编号取客户是同一个思路),以及根据参数返回不同的子类。
怎么一步步做
Section titled “怎么一步步做”每一步之后都跑测试。
- 新建工厂函数
create_employee(name, type_code),函数体只调用构造函数。 - 逐个把调用构造函数的地方改成调用工厂函数。搜索
Employee(就能找全;Java 的 IntelliJ IDEA 有现成的 Replace Constructor with Factory Method,Python 和 TS 的 IDE 一般没有,靠查找替换。 - 传字面量类型码的调用方,比如
Employee("老周", "E"),可以再建一个更具体的工厂函数create_engineer(name)改用它。 - 尽量限制构造函数的可见性。TS 可以把构造函数标成
private,此时工厂要写成类的静态方法;Python 没有私有构造函数,靠约定和文档说明“请用工厂创建”。
定义与适用场景
Section titled “定义与适用场景”以工厂函数取代构造函数:用一个普通函数代替构造函数来创建对象,调用方只调用这个函数。
它适合三种情况:创建时要根据参数决定返回哪个子类;创建时想复用已有对象或做缓存;同一个类有几种常见的创建方式,需要各自有个名字。Python 的 classmethod 备用构造器就是第三种的惯用写法,比如 datetime.fromtimestamp(ts)、dict.fromkeys(keys)、int.from_bytes(data, "big"),类名后面跟一个说明“从什么创建”的名字。
相关的设计模式
Section titled “相关的设计模式”本章的结果是简单工厂,也常被看作《设计模式》里工厂方法(Factory Method)模式的入门形态。严格地说,GoF 的工厂方法是父类里一个由子类重写的创建方法,让子类决定实例化哪个类;本章的 create_employee 是一个独立函数,没有这层继承。两者的共同点是“调用方不直接 new”,这也是本章所有好处的来源:一旦创建过程被收进一个函数,它就可以返回子类、可以缓存、可以起一个说明意图的名字。等到 create_employee 要根据类型码返回不同子类、而这个选择又需要随场景替换时,就该看工厂方法和抽象工厂了。工厂方法模式见《设计模式》书第 3 章。
真实系统里的例子
Section titled “真实系统里的例子”JavaScript 的 Array.of 和 Array.from
Section titled “JavaScript 的 Array.of 和 Array.from”new Array(...) 是构造函数难以表达意图的经典例子:只传一个数字时,它表示数组长度;传多个参数时,它们是数组元素。new Array(3) 得到长度为 3 的空数组,new Array(3, 4) 得到 [3, 4]。ES2015 为此加了两个工厂函数:Array.of 总是把参数当元素,Array.from 从可迭代对象或类数组对象创建。
// 构造函数的参数含义随个数变化;工厂函数的名字说清了意图。const a = new Array(3); // 长度为 3 的空数组const b = new Array(3, 4); // 元素 3 和 4const c = Array.of(3); // 元素 3const d = Array.from({ length: 3 }, (_, i) => i * 2); // 按长度生成
console.log(a.length, JSON.stringify(b));console.log(JSON.stringify(c), JSON.stringify(d));
// 输出:// 3 [3,4]// [3] [0,2,4]Array.of(3) 和 Array.from(...) 的名字本身就是文档。构造函数的毛病修不掉,因为要兼容旧代码,于是标准的做法正是本章的手法:保留构造函数,另给工厂函数,引导新代码用工厂。
什么时候别用
Section titled “什么时候别用”类只有一种创建方式、也没有子类和缓存的打算时,别加工厂。一个 Point(x, y) 包一层 create_point(x, y),只是多了一个名字要记,读代码的人还会好奇工厂里是不是藏着别的逻辑。Python 的数据类和 TS 的普通类,直接调用构造函数是最清楚的写法。等到真的需要返回子类或复用对象时再加,那时改动也只是本章四步。
- 构造函数的名字固定、只能返回本类的新实例;工厂函数能起说明意图的名字、能返回子类、能复用对象。
- 传字面量类型码的地方,换成
create_engineer这类具体的工厂函数,类型码从调用方消失。 - 先建工厂、再逐个替换调用方,最后尽量限制构造函数的使用。
- 它是第 52 章以子类取代类型码的准备步骤,也是工厂方法模式的起点。