Dark Dwarf Blog background

TypeScript Mixin

TypeScript Mixin

1. 引入

在经典的面向对象编程中,我们经常使用继承的方法:创建一个通用的基类,然后扩展它以创建更特化的类。然而,这种模型有其局限性。如果我们想从两个或多个不相关的类中组合功能,该怎么办?TypeScript 和 JavaScript 一样,只允许一个类扩展单个父类。这就是“组合优于继承”原则发挥作用的地方。而 Mixin 即是实现这一目标的强大模式。

与构建僵化的类层次结构不同,我们可以创建小的、可重用的功能片段(即 Mixin),并将它们“混入”到我们的类中以添加特定行为。

2. Mixin

a.a. 基本概念

Mixin 是一种设计模式,它允许一个类在不使用传统继承的情况下“借用”一个或多个其他类的功能。在 TypeScript 中,Mixin 通常实现为一个函数,该函数:

  1. 接受一个基类构造函数作为参数。
  2. 返回一个扩展了该基类的新类,并添加了新的属性和方法。

这种方法使我们能够从多个功能源组合类,从而实现一种形式的多重继承。

b.b. 简单实现

假设我们有一个 Person 类,并且我们希望添加数据库相关的功能,如“删除”状态和“更新”时间戳,而不使 Person 类本身变得混乱。

i.i. Constructor 类型

首先,我们定义一个通用的 Constructor 类型。这是在 TypeScript 中创建 Mixin 的标准约定。它代表任何可以用 new 调用的类构造函数。

type Constructor<T = {}> = new (...args: any[]) => T;

Constructor 这个类型代表了“任何一个可以被 new 的类”。我们用它告诉TypeScript:“我接下来要操作的目标是一个类,而不是一个普通函数或对象。”

因为 TypeScript 中主要是类可以被 new,这里就限制了只传入类。

ii.ii. 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();
  };
}

iii.iii. 组合 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

c.c. 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(); // 现在可以正常工作了
}