Typoskript: Wie kann man zwei Klassen erweitern?

85

Ich möchte meine Zeit sparen und allgemeinen Code klassenübergreifend wiederverwenden, wodurch PIXI-Klassen (eine 2D-WebGl-Renderer-Bibliothek) erweitert werden.

Objektschnittstellen:

module Game.Core {
    export interface IObject {}

    export interface IManagedObject extends IObject{
        getKeyInManager(key: string): string;
        setKeyInManager(key: string): IObject;
    }
}

Mein Problem ist, dass sich der Code darin befindet getKeyInManagerund setKeyInManagersich nicht ändert und ich ihn wiederverwenden und nicht duplizieren möchte. Hier ist die Implementierung:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

    public getKeyInManager(key: string): string{
        return this._keyInManager;
    }

    public setKeyInManager(key: string): DisplayObject{
        this._keyInManager = key;
        return this;
    }
}

Was ich tun möchte, ist, automatisch über a Manager.add()den Schlüssel hinzuzufügen, der im Manager verwendet wird, um das Objekt innerhalb des Objekts selbst in seiner Eigenschaft zu referenzieren _keyInManager.

Nehmen wir also ein Beispiel mit einer Textur. Hier geht dasTextureManager

module Game.Managers {
    export class TextureManager extends Game.Managers.Manager {

        public createFromLocalImage(name: string, relativePath: string): Game.Core.Texture{
            return this.add(name, Game.Core.Texture.fromImage("/" + relativePath)).get(name);
        }
    }
}

Wenn ich dies tue this.add(), möchte ich, dass die Game.Managers.Manager add()Methode eine Methode aufruft, die für das von zurückgegebene Objekt vorhanden ist Game.Core.Texture.fromImage("/" + relativePath). Dieses Objekt wäre in diesem Fall ein Texture:

module Game.Core {
    // I must extends PIXI.Texture, but I need to inject the methods in IManagedObject.
    export class Texture extends PIXI.Texture {

    }
}

Ich weiß, dass dies IManagedObjecteine Schnittstelle ist und keine Implementierung enthalten kann, aber ich weiß nicht, was ich schreiben soll, um die Klasse ObjectThatShouldAlsoBeExtendedin meine TextureKlasse einzufügen . Zu wissen , dass der gleiche Prozess für erforderlich wäre Sprite, TilingSprite, Layerund vieles mehr.

Ich benötige hier ein erfahrenes TypeScript-Feedback / Ratschläge, es muss möglich sein, aber nicht in mehreren Schritten, da zu diesem Zeitpunkt nur eines möglich ist. Ich habe keine andere Lösung gefunden.

Vadorequest
quelle
7
Nur ein Tipp: Wenn ich auf ein Problem mit Mehrfachvererbung stoße, versuche ich mich daran zu erinnern, dass ich "Komposition gegenüber Vererbung bevorzugen" soll, um zu sehen, ob dies den Job erledigt.
Bubbleking
2
Einverstanden.
Ich habe vor 2
6
@bubbleking Wie würde hier die Bevorzugung der Komposition gegenüber der Vererbung gelten?
Seanny123

Antworten:

93

In TypeScript gibt es eine wenig bekannte Funktion, mit der Sie Mixins verwenden können, um wiederverwendbare kleine Objekte zu erstellen. Sie können diese mithilfe der Mehrfachvererbung zu größeren Objekten zusammensetzen (Mehrfachvererbung ist für Klassen nicht zulässig, Mixins jedoch - wie Schnittstellen mit einer zugehörigen Implenentation).

Weitere Informationen zu TypeScript Mixins

Ich denke, Sie könnten diese Technik verwenden, um gemeinsame Komponenten zwischen vielen Klassen in Ihrem Spiel zu teilen und viele dieser Komponenten aus einer einzelnen Klasse in Ihrem Spiel wiederzuverwenden:

Hier ist eine kurze Mixins-Demo ... zuerst die Aromen, die Sie mischen möchten:

class CanEat {
    public eat() {
        alert('Munch Munch.');
    }
}

class CanSleep {
    sleep() {
        alert('Zzzzzzz.');
    }
}

Dann die magische Methode zur Mixin-Erstellung (Sie brauchen diese nur einmal irgendwo in Ihrem Programm ...)

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
             if (name !== 'constructor') {
                derivedCtor.prototype[name] = baseCtor.prototype[name];
            }
        });
    }); 
}

Und dann können Sie Klassen mit Mehrfachvererbung aus Mixin-Varianten erstellen:

class Being implements CanEat, CanSleep {
        eat: () => void;
        sleep: () => void;
}
applyMixins (Being, [CanEat, CanSleep]);

Beachten Sie, dass es in dieser Klasse keine tatsächliche Implementierung gibt - gerade genug, um die Anforderungen der "Schnittstellen" zu erfüllen. Aber wenn wir diese Klasse verwenden, funktioniert alles.

var being = new Being();

// Zzzzzzz...
being.sleep();
Fenton
quelle
3
Hier ist der Mixins-Abschnitt im TypeScript-Handbuch (aber Steve hat so ziemlich alles behandelt, was Sie in dieser Antwort und in seinem verlinkten Artikel wissen müssen) typescriptlang.org/Handbook#mixins
Troy Gizzi
2
Typescript 2.2 unterstützt
Flavien Volken
1
@FlavienVolken Wissen Sie, warum Microsoft den alten Mixins-Abschnitt in der Handbuchdokumentation beibehalten hat? Übrigens sind die Versionshinweise für einen Anfänger in TS wie mich wirklich schwer zu verstehen. Gibt es einen Link für ein Tutorial mit TS 2.2+ Mixins? Vielen Dank.
David D.
4
Die in diesem Beispiel gezeigte "alte Methode" zum Mischen von Mixins ist einfacher als die "neue Methode" (Typescript 2.2+). Ich weiß nicht, warum sie es so schwierig gemacht haben.
Tocqueville
2
Der Grund ist, dass der "alte Weg" den Typ nicht richtig bekommen kann.
unional
25

Ich würde vorschlagen, den dort beschriebenen neuen Mixins-Ansatz zu verwenden: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

Dieser Ansatz ist besser als der von Fenton beschriebene "applyMixins" -Ansatz, da der Autocompiler Ihnen helfen und alle Methoden / Eigenschaften sowohl der Basis- als auch der 2. Vererbungsklasse anzeigen würde.

Dieser Ansatz kann auf der TS Playground-Website überprüft werden .

Hier ist die Implementierung:

class MainClass {
    testMainClass() {
        alert("testMainClass");
    }
}

const addSecondInheritance = (BaseClass: { new(...args) }) => {
    return class extends BaseClass {
        testSecondInheritance() {
            alert("testSecondInheritance");
        }
    }
}

// Prepare the new class, which "inherits" 2 classes (MainClass and the cass declared in the addSecondInheritance method)
const SecondInheritanceClass = addSecondInheritance(MainClass);
// Create object from the new prepared class
const secondInheritanceObj = new SecondInheritanceClass();
secondInheritanceObj.testMainClass();
secondInheritanceObj.testSecondInheritance();
Mark Dolbyrev
quelle
Ist SecondInheritanceClassnicht absichtlich definiert oder fehlt mir etwas? Beim Laden dieses Codes auf den TS-Spielplatz heißt es expecting =>. Könnten Sie schließlich genau aufschlüsseln, was in der addSecondInheritanceFunktion passiert , beispielsweise was ist der Zweck von new (...args)?
Seanny123
Der Hauptpunkt einer solchen Mixin-Implementierung ist, dass ALLE Methoden und Eigenschaften der beiden Klassen in der IDE-Hilfe zur automatischen Vervollständigung angezeigt werden. Wenn Sie möchten, können Sie die 2. Klasse definieren und den von Fenton vorgeschlagenen Ansatz verwenden. In diesem Fall funktioniert die automatische Vervollständigung der IDE jedoch nicht. {new (... args)} - dieser Code ein Objekt beschreibt , die eine Klasse sein sollten (Sie TS - Schnittstellen mehr im Handbuch lesen können: typescriptlang.org/docs/handbook/interfaces.html
Mark Dolbyrev
2
Das Problem hierbei ist, dass TS immer noch keine Ahnung von der geänderten Klasse hat. Ich kann tippen secondInheritanceObj.some()und bekomme keine Warnmeldung.
SF
Wie überprüfe ich, ob die gemischte Klasse Schnittstellen gehorcht?
Rilut
11
Dieser 'neue Mixins-Ansatz' von Typescript sieht nach einem nachträglichen Gedanken aus. Als Entwickler möchte ich nur sagen können: "Ich möchte, dass diese Klasse ClassA und ClassB erbt" oder "Ich möchte, dass diese Klasse eine Mischung aus ClassA und ClassB ist", und ich möchte dies mit einer klaren Syntax ausdrücken, an die ich mich erinnern kann in 6 Monaten nicht das Hokuspokus. Wenn es sich um eine technische Einschränkung der Möglichkeiten des TS-Compilers handelt, ist dies keine Lösung.
Rui Marques
12

Leider unterstützt Typoskript keine Mehrfachvererbung. Daher gibt es keine völlig triviale Antwort, Sie müssen wahrscheinlich Ihr Programm umstrukturieren

Hier einige Vorschläge:

  • Wenn diese zusätzliche Klasse ein Verhalten enthält, das viele Ihrer Unterklassen gemeinsam haben, ist es sinnvoll, es irgendwo oben in die Klassenhierarchie einzufügen. Vielleicht könnten Sie aus dieser Klasse die übliche Oberklasse von Sprite, Texture, Layer, ... ableiten? Dies wäre eine gute Wahl, wenn Sie einen guten Platz in der Typ-Hirarchie finden könnten. Ich würde jedoch nicht empfehlen, diese Klasse nur an einer zufälligen Stelle einzufügen. Vererbung drückt eine "Ist eine - Beziehung" aus, zB ein Hund ist ein Tier, eine Textur ist eine Instanz dieser Klasse. Sie müssten sich fragen, ob dies wirklich die Beziehung zwischen den Objekten in Ihrem Code modelliert. Ein logischer Vererbungsbaum ist sehr wertvoll

  • Wenn die zusätzliche Klasse nicht logisch in die Typhierarchie passt, können Sie die Aggregation verwenden. Das bedeutet, dass Sie eine Instanzvariable vom Typ dieser Klasse zu einer allgemeinen Oberklasse von Sprite, Texture, Layer, ... hinzufügen. Anschließend können Sie in allen Unterklassen auf die Variable mit ihrem Getter / Setter zugreifen. Dies modelliert eine "Hat eine - Beziehung".

  • Sie können Ihre Klasse auch in eine Schnittstelle konvertieren. Dann könnten Sie die Schnittstelle mit all Ihren Klassen erweitern, müssten aber die Methoden in jeder Klasse korrekt implementieren. Dies bedeutet eine gewisse Code-Redundanz, in diesem Fall jedoch nicht viel.

Sie müssen selbst entscheiden, welcher Ansatz Ihnen am besten gefällt. Persönlich würde ich empfehlen, die Klasse in eine Schnittstelle zu konvertieren.

Ein Tipp: Typescript bietet Eigenschaften, die syntaktischer Zucker für Getter und Setter sind. Vielleicht möchten Sie einen Blick darauf werfen: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/

lhk
quelle
2
Interessant. 1) Ich kann das nicht tun, einfach weil ich erweiterte PIXIund die Bibliothek nicht ändern kann, um eine weitere Klasse darüber hinzuzufügen. 2) Es ist eine der möglichen Lösungen, die ich verwenden könnte, aber ich hätte es vorgezogen, wenn möglich zu vermeiden. 3) Ich möchte diesen Code definitiv nicht duplizieren, es mag jetzt einfach sein, aber was wird als nächstes passieren? Ich habe nur einen Tag an diesem Programm gearbeitet und ich werde später noch viel mehr hinzufügen, keine gute Lösung für mich. Ich werde mir den Tipp ansehen, danke für die detaillierte Antwort.
Vadorequest
Interessanter Link in der Tat, aber ich sehe hier keinen Anwendungsfall, um das Problem zu lösen.
Vadorequest
Wenn alle diese Klassen PIXI erweitern, lassen Sie die ObjectThatShouldAlsoBeExtended-Klasse PIXI erweitern und leiten Sie daraus die Klassen Texture, Sprite, ... ab. Das habe ich mit dem Einfügen der Klasse in die Typ-Hirarchie gemeint
lhk
PIXIselbst ist keine Klasse, es ist ein Modul, es kann nicht erweitert werden, aber Sie haben Recht, das wäre eine praktikable Option gewesen, wenn es gewesen wäre!
Vadorequest
1
Also erweitert jede der Klassen eine andere Klasse aus dem PIXI-Modul? Dann haben Sie Recht, Sie können die ObjectThatShouldAlso-Klasse nicht in die Typhirarchie einfügen. Das ist doch nicht möglich. Sie können weiterhin die Has-A-Beziehung oder die Schnittstelle auswählen. Da Sie ein gemeinsames Verhalten beschreiben möchten, das alle Ihre Klassen gemeinsam haben, würde ich die Verwendung einer Schnittstelle empfehlen. Das ist das sauberste Design, auch wenn Code dupliziert wird.
lhk
10

TypeScript unterstützt Dekoratoren . Mit dieser Funktion und einer kleinen Bibliothek namens Typoskript-Mix können Sie Mixins verwenden, um mit nur wenigen Zeilen mehrere Vererbungen durchzuführen

// The following line is only for intellisense to work
interface Shopperholic extends Buyer, Transportable {}

class Shopperholic {
  // The following line is where we "extend" from other 2 classes
  @use( Buyer, Transportable ) this 
  price = 2000;
}
Ivan Castellanos
quelle
7

Ich denke, es gibt einen viel besseren Ansatz, der solide Typensicherheit und Skalierbarkeit ermöglicht.

Deklarieren Sie zunächst Schnittstellen, die Sie in Ihrer Zielklasse implementieren möchten:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

Jetzt müssen wir die Implementierung zur FooKlasse hinzufügen . Wir können Klassenmixins verwenden, die auch diese Schnittstellen implementieren:

class Base {}

type Constructor<I = Base> = new (...args: any[]) => I;

function Bar<T extends Constructor>(constructor: T = Base as any) {
  return class extends constructor implements IBar {
    public doBarThings() {
      console.log("Do bar!");
    }
  };
}

function Bazz<T extends Constructor>(constructor: T = Base as any) {
  return class extends constructor implements IBazz {
    public doBazzThings() {
      console.log("Do bazz!");
    }
  };
}

Erweitern Sie die FooKlasse mit den Klassenmixins:

class Foo extends Bar(Bazz()) implements IBar, IBazz {
  public doBarThings() {
    super.doBarThings();
    console.log("Override mixin");
  }
}

const foo = new Foo();
foo.doBazzThings(); // Do bazz!
foo.doBarThings(); // Do bar! // Override mixin
Nomadoda
quelle
Was ist der zurückgegebene Typ der Funktionen Bar und Bazz?
Luke Skywalker
3

Eine sehr hackige Lösung wäre, die Klasse zu durchlaufen, die Sie erben möchten, indem Sie die Funktionen einzeln zur neuen übergeordneten Klasse hinzufügen

class ChildA {
    public static x = 5
}

class ChildB {
    public static y = 6
}

class Parent {}

for (const property in ChildA) {
    Parent[property] = ChildA[property]
}
for (const property in ChildB) {
    Parent[property] = ChildB[property]
}


Parent.x
// 5
Parent.y
// 6

Alle Eigenschaften von ChildAund ChildBkönnen jetzt von der ParentKlasse aus aufgerufen werden. Sie werden jedoch nicht erkannt, was bedeutet, dass Sie Warnungen wie zProperty 'x' does not exist on 'typeof Parent'

WillCooter
quelle
1

In Entwurfsmustern gibt es ein Prinzip namens "Komposition gegenüber Vererbung bevorzugen". Anstatt Klasse B von Klasse A zu erben, wird eine Instanz der Klasse A in die Klasse B als Eigenschaft eingefügt. Anschließend können Sie die Funktionen der Klasse A in der Klasse B verwenden. Hier und hier finden Sie einige Beispiele dafür .

AmirHossein Rezaei
quelle
0

Es gibt hier bereits so viele gute Antworten, aber ich möchte nur anhand eines Beispiels zeigen, dass Sie der Klasse, die erweitert wird, zusätzliche Funktionen hinzufügen können.

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            if (name !== 'constructor') {
                derivedCtor.prototype[name] = baseCtor.prototype[name];
            }
        });
    });
}

class Class1 {
    doWork() {
        console.log('Working');
    }
}

class Class2 {
    sleep() {
        console.log('Sleeping');
    }
}

class FatClass implements Class1, Class2 {
    doWork: () => void = () => { };
    sleep: () => void = () => { };


    x: number = 23;
    private _z: number = 80;

    get z(): number {
        return this._z;
    }

    set z(newZ) {
        this._z = newZ;
    }

    saySomething(y: string) {
        console.log(`Just saying ${y}...`);
    }
}
applyMixins(FatClass, [Class1, Class2]);


let fatClass = new FatClass();

fatClass.doWork();
fatClass.saySomething("nothing");
console.log(fatClass.x);
Tebo
quelle