javascript - Typescript: Cum să extinzi două clase?


Vreau să economisesc timp și să reutilizez cod comun în clasele care extind clasele PIXI (o bibliotecă de redare 2D WebGL).

Interfețe de obiect:

module Game.Core {
    export interface IObject {}

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

Problema mea este că codul din interiorul getKeyInManager și setKeyInManager nu se va schimba și vreau să-l refolosesc, nu să-l duplic, iată implementarea:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

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

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

Ceea ce vreau să fac este să adaug automat, printr-un Manager.add(), cheia folosită în manager pentru a referenția obiectul în interiorul obiectului însuși, în proprietatea sa _keyInManager.

Așadar, să luăm un exemplu cu un Texture. Iată TextureManager

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);
        }
    }
}

Când fac this.add(), vreau ca metoda add() din Game.Managers.Manager să apeleze o metodă care ar exista pe obiectul returnat de Game.Core.Texture.fromImage("/" + relativePath). Acest obiect, în acest caz, ar fi un Texture:

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

    }
}

Știu că IManagedObject este o interfață și nu poate conține implementare, dar nu știu ce să scriu pentru a injecta clasa ObjectThatShouldAlsoBeExtended în clasa mea Texture. Știind că același proces ar fi necesar și pentru Sprite, TilingSprite, Layer și altele.

Am nevoie de feedback/sfaturi despre TypeScript de la cineva experimentat aici, trebuie să fie posibil de realizat, dar nu prin multiple extends deoarece este posibil doar unul la un moment dat, nu am găsit nicio altă soluție.

Răspuns #1

Există o caracteristică puțin cunoscută în TypeScript care îți permite să folosești Mixins pentru a crea obiecte mici reutilizabile. Le poți compune în obiecte mai mari folosind moștenirea multiplă (moștenirea multiplă nu este permisă pentru clase, dar este permisă pentru mixins - care sunt ca interfețele cu o implementare asociată).

Mai multe informații despre Mixins în TypeScript

Cred că ai putea folosi această tehnică pentru a partaja componente comune între multe clase din jocul tău și pentru a reutiliza multe dintre aceste componente dintr-o singură clasă din jocul tău:

Iată o demonstrație rapidă a Mixins... mai întâi, aromele pe care doriți să le amestecați:

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

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

Apoi metoda magică pentru crearea Mixin-ului (ai nevoie de aceasta doar o dată undeva în programul tău...)

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

Și atunci poți crea clase cu moștenire multiplă din diferite tipuri de mixin:

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

Rețineți că nu există o implementare efectivă în această clasă - doar suficient pentru a îndeplini cerințele „interfețelor”. Dar când folosim această clasă - totul funcționează.

var being = new Being();

// Zzzzzzz.
being.sleep();

Răspunsul #2

Aș sugera să folosești noua abordare cu mixinuri descrisă acolo: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

Această abordare este mai bună decât abordarea „applyMixins” descrisă de Fenton, deoarece autocompilatorul te-ar ajuta și ar arăta toate metodele/proprietățile atât din clasa de bază, cât și din a doua clasă de moștenire.

Această abordare ar putea fi verificată pe site-ul TS Playground.

Iată implementarea:

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();

Răspunsul #3

TypeScript acceptă decoratori, iar folosind această caracteristică plus o mică bibliotecă numită typescript-mix, poți folosi mixin-uri pentru a avea moștenire multiplă cu doar câteva linii

// 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;
}

Răspunsul #4

Din păcate, TypeScript nu suportă moștenirea multiplă. Prin urmare, nu există un răspuns complet trivial, probabil va trebui să-ți restructurezi programul.

Iată câteva sugestii:

  • Dacă această clasă suplimentară conține comportamente pe care multe dintre subclasele tale le împărtășesc, are sens să o inserezi în ierarhia claselor, undeva mai sus. Poate ai putea deriva superclasa comună a Sprite, Texture, Layer, ... din această clasă? Aceasta ar fi o alegere bună, dacă poți găsi un loc potrivit în ierarhia tipurilor. Dar nu aș recomanda să inserezi această clasă la întâmplare. Moștenirea exprimă o relație de tipul „Este un/ o”, de exemplu, un câine este un animal, o textură este o instanță a acestei clase. Ar trebui să te întrebi dacă modelul relației dintre obiectele din codul tău este cu adevărat descris corect de această moștenire. Un arbore de moștenire logic este foarte valoros.

  • Dacă clasa suplimentară nu se potrivește logic în ierarhia de tipuri, ai putea folosi agregarea. Asta înseamnă că adaugi o variabilă de instanță de tipul acestei clase într-o superclasă comună a Sprite, Texture, Layer, ... Apoi poți accesa variabila cu getter-ul/setter-ul său în toate subclasele. Aceasta modelează o relație de tip „Are un”.

  • Ai putea, de asemenea, să convertești clasa ta într-o interfață. Apoi ai putea extinde interfața cu toate clasele tale, dar ar trebui să implementezi metodele corect în fiecare clasă. Aceasta înseamnă unele redundanțe de cod, dar în acest caz nu prea multe.

Trebuie să decizi singur care abordare îți place cel mai mult. Personal, aș recomanda să convertești clasa într-o interfață.

Un sfat: TypeScript oferă proprietăți, care sunt zahăr sintactic pentru getter și setter. S-ar putea să vrei să arunci o privire aici: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/

Răspuns #5

Cred că există o abordare mult mai bună, care permite siguranță solidă a tipurilor și scalabilitate.

Mai întâi declară interfețele pe care vrei să le implementezi în clasa ta țintă:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

Acum trebuie să adăugăm implementarea la clasa Foo. Putem folosi mixin-uri de clasă care implementează, de asemenea, aceste interfețe:

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!");
    }
  };
}

Extinde clasa Foo cu mixin-urile de clasă:

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