Миксины
Наряду с традиционными иерархиями ООП, еще один популярный способ построения классов из переиспользуемых компонентов — объединение более простых частичных классов. Вы, возможно, знакомы с концепцией миксинов или трейтов в языках вроде Scala, и этот паттерн также получил популярность в сообществе JavaScript.
Как работают миксины?
Этот паттерн опирается на использование дженериков с наследованием классов для расширения базового класса. Лучшая поддержка миксинов в TypeScript реализуется с помощью паттерна выражения класса. Вы можете узнать больше о работе этого паттерна в JavaScript здесь.
Для начала нам понадобится класс, на который будут накладываться миксины:
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
} Затем вам нужен тип и функция-фабрика, которая возвращает выражение класса, расширяющее базовый класс.
// To get started, we need a type which we'll use to extend
// other classes from. The main responsibility is to declare
// that the type being passed in is a class.
type Constructor = new (...args: any[]) => {};
// This mixin adds a scale property, with getters and setters
// for changing it with an encapsulated private property:
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;
setScale(scale: number) {
this._scale = scale;
}
get scale(): number {
return this._scale;
}
};
} После настройки вы можете создать класс, представляющий базовый класс с применёнными миксинами:
// Compose a new class from the Sprite class,
// with the Mixin Scale applier:
const EightBitSprite = Scale(Sprite);
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale); Ограниченные миксины
В вышеприведённом виде миксины не обладают никакими знаниями о классе, что может затруднить создание желаемого дизайна.
Чтобы смоделировать это, мы изменяем исходный тип конструктора, чтобы он принимал дженерический аргумент.
// This was our previous constructor:
type Constructor = new (...args: any[]) => {};
// Now we use a generic version which can apply a constraint on
// the class which this mixin is applied to
type GConstructor<T = {}> = new (...args: any[]) => T; Это позволяет создавать классы, которые работают только с ограниченными базовыми классами:
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>; Затем вы можете создать миксины, которые работают только при наличии определенного базового класса:
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// This mixin will only work if it is passed a base
// class which has setPos defined because of the
// Positionable constraint.
this.setPos(0, 20);
}
};
} Альтернативный паттерн
Предыдущие версии этого документа рекомендовали способ написания миксинов, где вы создавали отдельные временные и типовые иерархии, а затем объединяли их в конце:
// Each mixin is a traditional ES class
class Jumpable {
jump() {}
}
class Duckable {
duck() {}
}
// Including the base
class Sprite {
x = 0;
y = 0;
}
// Then you create an interface which merges
// the expected mixins with the same name as your base
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins into the base class via
// the JS at runtime
applyMixins(Sprite, [Jumpable, Duckable]);
let player = new Sprite();
player.jump();
console.log(player.x, player.y);
// This can live anywhere in your codebase:
function applyMixins(derivedCtor: any, constructors: any[]) {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null)
);
});
});
} Этот паттерн меньше полагается на компилятор и больше на ваш код, чтобы гарантировать, что временная и система типов корректно синхронизированы.
Ограничения
Паттерн миксинов поддерживается в TypeScript-компиляторе напрямую за счёт анализа потоков кода. Есть несколько случаев, когда вы можете столкнуться с границами этой поддержки.
Декораторы и миксины #4881
Вы не можете использовать декораторы для предоставления миксинов через анализ потоков кода:
// A decorator function which replicates the mixin pattern:
const Pausable = (target: typeof Player) => {
return class Pausable extends target {
shouldFreeze = false;
};
};
@Pausable
class Player {
x = 0;
y = 0;
}
// The Player class does not have the decorator's type merged:
const player = new Player();
player.shouldFreeze;
// The runtime aspect could be manually replicated via
// type composition or interface merging.
type FreezablePlayer = Player & { shouldFreeze: boolean };
const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze; Статические миксины свойств #17829
Скорее, это подводный камень, чем ограничение. Паттерн выражения класса создаёт синглтоны, поэтому их нельзя сопоставить в системе типов, чтобы поддержать разные типы переменных.
Вы можете обойти это, используя функции, возвращающие классы, которые отличаются в зависимости от дженерика:
function base<T>() {
class Base {
static prop: T;
}
return Base;
}
function derived<T>() {
class Derived extends base<T>() {
static anotherProp: T;
}
return Derived;
}
class Spec extends derived<string>() {}
Spec.prop; // string
Spec.anotherProp; // string
© 2012-2024 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/mixins.html