TypeScript Mixin
1. 引入
在经典的面向对象编程中,我们经常使用继承的方法:创建一个通用的基类,然后扩展它以创建更特化的类。然而,这种模型有其局限性。如果我们想从两个或多个不相关的类中组合功能,该怎么办?TypeScript 和 JavaScript 一样,只允许一个类扩展单个父类。这就是“组合优于继承”原则发挥作用的地方。而 Mixin 即是实现这一目标的强大模式。
与构建僵化的类层次结构不同,我们可以创建小的、可重用的功能片段(即 Mixin),并将它们“混入”到我们的类中以添加特定行为。
2. Mixin
基本概念
Mixin 是一种设计模式,它允许一个类在不使用传统继承的情况下“借用”一个或多个其他类的功能。在 TypeScript 中,Mixin 通常实现为一个函数,该函数:
- 接受一个基类构造函数作为参数。
- 返回一个扩展了该基类的新类,并添加了新的属性和方法。
这种方法使我们能够从多个功能源组合类,从而实现一种形式的多重继承。
简单实现
假设我们有一个 Person 类,并且我们希望添加数据库相关的功能,如“删除”状态和“更新”时间戳,而不使 Person 类本身变得混乱。
Constructor 类型
首先,我们定义一个通用的 Constructor 类型。这是在 TypeScript 中创建 Mixin 的标准约定。它代表任何可以用 new 调用的类构造函数。
type Constructor<T = {}> = new (...args: any[]) => T;
Constructor这个类型代表了“任何一个可以被new的类”。我们用它告诉TypeScript:“我接下来要操作的目标是一个类,而不是一个普通函数或对象。”
因为 TypeScript 中主要是类可以被
new,这里就限制了只传入类。
Mixin 函数
现在,让我们创建第一个 Mixin,RecordStatus。它是一个函数,接受一个基类并返回一个具有“软删除”功能的新类。
function RecordStatus<TBase extends Constructor>(Base: TBase) {
return class extends Base {
private deleted: boolean = false;
get Deleted(): boolean {
return this.deleted;
}
Delete(): void {
this.deleted = true;
console.log("该记录已被标记为删除。");
}
};
}
在这里,RecordStatus 是一个返回类的高阶函数。<TBase extends Constructor> 这个泛型约束确保我们传入的是一个真正的类,而不是别的东西。然后我们通过在函数内部创建一个匿名类,在继承原有类 Base 的所有方法外加上自己的方法。
然后创建另一个用于时间戳的 Mixin。
function Timestamp<TBase extends Constructor>(Base: TBase) {
return class extends Base {
Updated: Date = new Date();
};
}
组合 Mixin
然后我们将它们应用于我们的基类 Person。Mixin 的应用顺序是从内到外。
class Person {
constructor(
public FirstName: string,
public LastName: string,
) {}
}
// 将 Mixin 应用于 Person 类
const TimestampedPerson = Timestamp(Person);
const ActivePerson = RecordStatus(TimestampedPerson);
// 或者,更简洁地:
// const ActivePerson = RecordStatus(Timestamp(Person));
// 现在,创建组合类的实例
const activePerson = new ActivePerson("Peter", "O'Hanlon");
// 我们现在可以访问所有来源的属性和方法
console.log(activePerson.FirstName); // "Peter" (来自 Person)
console.log(activePerson.Updated); // 当前日期 (来自 Timestamp)
activePerson.Delete(); // "该记录已被标记为删除。" (来自 RecordStatus)
console.log(activePerson.Deleted); // true
Mixin 的优点
- 代码可重用性:一次性定义一个行为(如日志记录或时间戳),并在许多不相关的类中重用它。
- 模块化与关注点分离:每个 Mixin 都有单一的职责,使代码更清晰、更易于维护。
- 克服单一继承的限制:Mixin 提供了一种在类之间共享功能的灵活方式,有效地模拟了多重继承。
- 动态组合:我们可以动态选择要应用于类的 Mixin,从而创建高度定制化的对象。
3. Mixin 的局限性
Mixin 只是将有不同功能的类组装到一起,它并没有实际修改我们在构造(比如前面的 ActivePerson)类的时候传入的东西,因此下面的代码会报错:
// 这将导致类型错误,因为 'person' 参数的类型
// 并不知道它有一个 'Delete' 方法。
function DeletePerson(person: Person) {
// person.Delete(); // 错误: 类型 'Person' 上不存在属性 'Delete'。
}
当然更不可以传入
ActivePerson,这是一个构造函数,是值而不是类!
为了解决这个问题,我们可以创建一个代表组合类的新类型。一种常见的方法是使用 InstanceType 来帮助创建一个组合类型。
// 为组合类创建一个类型
type ActivePersonType = InstanceType<typeof ActivePerson>;
function DeletePerson(person: ActivePersonType) {
person.Delete(); // 现在可以正常工作了
}