TypeScript-Mixin
Ein Mixin ist ein Muster zur Code-Wiederverwendung, mit dem mehrere unabhängige Funktionsmodule in eine einzelne Klasse gemischt werden.
TypeScript implementiert Mixins, indem generische Funktionen verwendet werden, um erweiterte Klassen zurückzugeben, was die Einschränkung ausgleicht, dass Einfachvererbung Verhalten aus mehreren Quellen nicht wiederverwenden kann.
SVG-Diagramm: Mixin-KompositionsprozessGrundlegende Konzepte
Der Kern eines Mixins ist eine generische Funktion, die eine Basisklasse entgegennimmt und eine erweiterte Klasse zurückgibt.ConstructorDie Typbeschränkung wird verwendet, um "jede Klasse, die vererbt werden kann" zu beschreiben.
Beispiel
type Constructor<T = {}> = new (...args: any[]) => T;
// Mixin-Funktion: nimmt eine Basisklasse entgegen und gibt eine neue erweiterte Klasse zurück
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
// Basisklasse
class User {
constructor(public name: string) {}
}
// Neue Klasse nach dem Einmischen abrufen
const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");
console.log(user.name); // Alice
console.log(user.createdAt instanceof Date); // true
Ausgabe:
Alice true
Hinweis:Ein Mixin ändert die ursprüngliche Klasse nicht; es gibt eine brandneue erweiterte Klasse zurück. Die ursprüngliche
UserKlasse ist nicht betroffen.
Mehrere Mixins kombinieren
Das Verschachteln mehrerer Mixin-Funktionen ermöglicht es, mehrere Fähigkeiten nacheinander auf dieselbe Klasse zu stapeln.
Beispiel
// Mixin 1: Zeitstempel hinzufügen
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
// Mixin 2: Serialisierung hinzufügen
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this);
}
};
}
// Mixin 3: Protokollierung hinzufügen (hängt von der serialize-Methode ab)
function Loggable<TBase extends Constructor<{ serialize(): string }>>(Base: TBase) {
return class extends Base {
log(): void {
console.log("[LOG]", this.serialize());
}
};
}
class Product {
constructor(public name: string, public price: number) {}
}
// Drei Mixins nacheinander stapeln
const AdvancedProduct = Loggable(Serializable(Timestamped(Product)));
const p = new AdvancedProduct("Phone", 999);
p.log();
console.log(p.createdAt instanceof Date); // true
Ausgabe:
[LOG] {"name":"Phone","price":999,"createdAt":"2026-..."}
true
Mixin und Schnittstellen
Die von einem Mixin zurückgegebene Klasse kann deklarieren, dass sie eine Schnittstelle implementiert, wodurch Konsumenten mit dem gemischten Objekt über den Schnittstellentyp arbeiten können, ohne von der konkreten Implementierung abhängig zu sein.
Beispiel
// Eine Fähigkeits-Schnittstelle definieren
interface ISerializable {
serialize(): string;
}
interface ICloneable<T> {
clone(): T;
}
// Mixin implementiert das Interface
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base implements ISerializable {
serialize(): string {
return JSON.stringify(this);
}
};
}
function Cloneable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
clone() {
return Object.assign(
Object.create(Object.getPrototypeOf(this)),
this
);
}
};
}
class Article {
constructor(public title: string, public content: string) {}
}
const RichArticle = Cloneable(Serializable(Article));
const a1 = new RichArticle("TypeScript Einführung", "Inhalt...");
const a2 = a1.clone();
a2.title = "TypeScript für Fortgeschrittene";
console.log(a1.serialize());
// {"title":"TypeScript-Einführung","content":"Inhalt..."}
console.log(a2.serialize());
// {"title":"TypeScript für Fortgeschrittene","content":"Inhalt..."}
console.log(a1.title === a2.title); // false (nach dem Klonen unabhängig geändert)
Ausgabe:
{"title":"TypeScript 入门","content":"正文内容..."}
{"title":"TypeScript 进阶","content":"正文内容..."}
false
Eingeschränktes Mixin
Mithilfe generischer Einschränkungen können Sie ein Mixin darauf beschränken, nur in Klassen eingemischt zu werden, die bestimmte Bedingungen erfüllen, wodurch fehlende erforderliche Eigenschaften zur Laufzeit vermieden werden.
Beispiel
// Einschränkung: Die Basisklasse muss die Eigenschaften id und name haben
type WithIdAndName = Constructor<{ id: number; name: string }>;
function Printable<TBase extends WithIdAndName>(Base: TBase) {
return class extends Base {
print(): void {
console.log(`[${this.id}] ${this.name}`);
}
};
}
class Item {
constructor(public id: number, public name: string) {}
}
// Korrekt: Item erfüllt die Einschränkung
const PrintableItem = Printable(Item);
const item = new PrintableItem(42, "Keyboard");
item.print(); // [42] Keyboard
// Fehlerbeispiel (der Compiler wird es verhindern):
// class NoId { constructor(public name: string) {} }
// const Bad = Printable(NoId); // Fehler: NoId fehlt die id-Eigenschaft
Ausgabe:
[42] Keyboard
Mixin vs. Vererbung
| Dimension | Vererbung (extends) | Mixin |
|---|---|---|
| Anzahl der Quellen | Kann nur von einer Elternklasse erben | Kann beliebig viele stapeln |
| Kopplungsgrad | Starke Kopplung zwischen Unterklasse und Elternklasse | Jedes Mixin ist unabhängig, geringe Kopplung |
| Wiederverwendungsgranularität | Wiederverwendung der Fähigkeiten der gesamten Klasse | Wiederverwendung einer einzelnen Funktion bei Bedarf |
| Typsicherheit | Native Unterstützung | Muss über generische Einschränkungen sichergestellt werden |
| Anwendbare Szenarien | Starke "is-a"-Beziehung | Querschnittsbelange (Logging, Serialisierung, Caching usw.) |
Zusammenfassung
- Kernmuster:Ein Mixin ist eine generische Funktion, die eine Basisklasse entgegennimmt und eine erweiterte Klasse zurückgibt,
Constructor<T>ist der Standard-Constraint-Typ - Fähigkeits-Stapelung:Das Verschachteln mehrerer Mixin-Funktionen ermöglicht es, mehrere Fähigkeiten in derselben Klasse zu kombinieren
- Interface-Integration:Die von einem Mixin zurückgegebene Klasse kann Interfaces implementieren; Konsumenten müssen nur von Interfaces abhängen, nicht von konkreten Klassen
- Generische Constraints:Durch Constraints,
TBasekann man den anwendbaren Bereich eines Mixins einschränken und eine falsche Verwendung zur Kompilierungszeit verhindern - Anwendbare Szenarien:Protokollierung, Serialisierung, Klonen, Zeitstempel und andere querschnittliche Belange; besser als mehrstufige Vererbung