Dattiloscritto: come estendere due classi?


90

Voglio risparmiare tempo e riutilizzare il codice comune tra le classi che estende le classi PIXI (una libreria di rendering 2d webGl).

Interfacce oggetto:

module Game.Core {
    export interface IObject {}

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

Il mio problema è che il codice all'interno getKeyInManagere setKeyInManagernon cambierà e voglio riutilizzarlo, non duplicarlo, ecco l'implementazione:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

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

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

Quello che voglio fare è aggiungere automaticamente, tramite a Manager.add(), la chiave usata nel gestore per referenziare l'oggetto all'interno dell'oggetto stesso nella sua proprietà_keyInManager .

Quindi, facciamo un esempio con una Texture. Ecco il fileTextureManager

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

Quando lo faccio this.add(), voglio che il Game.Managers.Manager add()metodo chiami un metodo che esisterebbe sull'oggetto restituito da Game.Core.Texture.fromImage("/" + relativePath). Questo oggetto, in questo caso, sarebbe un Texture:

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

    }
}

So che IManagedObjectè un'interfaccia e non può contenere l'implementazione, ma non so cosa scrivere per iniettare la classe ObjectThatShouldAlsoBeExtendedall'interno della mia Textureclasse. Sapendo che lo stesso processo sarebbe richiesto per Sprite, TilingSprite, Layere altro ancora.

Ho bisogno di un feedback / consigli TypeScript con esperienza qui, deve essere possibile farlo, ma non per più estensioni poiché solo una è possibile al momento, non ho trovato nessun'altra soluzione.


7
Solo un suggerimento, ogni volta che mi imbatto in un problema di ereditarietà multipla, cerco di ricordare a me stesso di pensare "preferire la composizione all'ereditarietà" per vedere se funziona.
Bubbleking

2
Concordato. Non pensavo in quel modo 2 anni fa;)
Vadorequest

6
@bubbleking come si applicherebbe qui favorire la composizione rispetto all'ereditarietà?
Seanny123

Risposte:


97

C'è una caratteristica poco conosciuta in TypeScript che ti permette di usare Mixin per creare piccoli oggetti riutilizzabili. È possibile comporli in oggetti più grandi utilizzando l'ereditarietà multipla (l'ereditarietà multipla non è consentita per le classi, ma è consentita per i mixin, che sono come le interfacce con un'implementazione associata).

Ulteriori informazioni sui mixin TypeScript

Penso che potresti usare questa tecnica per condividere componenti comuni tra molte classi nel tuo gioco e per riutilizzare molti di questi componenti da una singola classe nel tuo gioco:

Ecco una rapida demo di Mixins ... prima, i sapori che vuoi mescolare:

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

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

Quindi il metodo magico per la creazione di Mixin (ne hai bisogno solo una volta da qualche parte nel tuo programma ...)

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

E poi puoi creare classi con ereditarietà multipla dai gusti mixin:

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

Notare che non esiste alcuna implementazione effettiva in questa classe, quanto basta per far sì che soddisfi i requisiti delle "interfacce". Ma quando usiamo questa classe, funziona tutto.

var being = new Being();

// Zzzzzzz...
being.sleep();

3
Ecco la sezione Mixins nel Manuale di TypeScript (ma Steve ha praticamente coperto tutto ciò che devi sapere in questa risposta e nel suo articolo collegato) typescriptlang.org/Handbook#mixins
Troy Gizzi

2
Typescript 2.2 ora supporta Mixins
Flavien Volken

1
@FlavienVolken Sapete perché Microsoft ha mantenuto la vecchia sezione mixin nella documentazione del manuale? A proposito, le note di rilascio sono davvero difficili da capire per un principiante in TS come me. Qualche collegamento per un tutorial con i mixin di TS 2.2+? Grazie.
David D.

4
Il "vecchio modo" di fare i mixin mostrato in questo esempio è più semplice del "nuovo modo" (Typescript 2.2+). Non so perché l'hanno reso così difficile.
tocqueville

2
Il motivo è che il "vecchio modo" non riesce a ottenere il tipo correttamente.
unional

25

Suggerirei di utilizzare il nuovo approccio mixin qui descritto: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

Questo approccio è migliore dell'approccio "applyMixins" descritto da Fenton, perché l'autocompilatore ti aiuterebbe e mostrerebbe tutti i metodi / proprietà dalle classi di ereditarietà di base e 2a.

Questo approccio potrebbe essere verificato sul sito di TS Playground .

Ecco l'implementazione:

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

Non SecondInheritanceClassè definito apposta o mi manca qualcosa? Quando si carica questo codice nel playground di TS, dice expecting =>. Infine, potresti analizzare esattamente ciò che sta accadendo nella addSecondInheritancefunzione, ad esempio qual è lo scopo di new (...args)?
Seanny123

Il punto principale di tale implementazione di mixin è che TUTTI i metodi e le proprietà di entrambe le classi saranno mostrati nella guida IDE di completamento automatico. Se vuoi, puoi definire la 2a classe e utilizzare l'approccio suggerito da Fenton, ma in questo caso il completamento automatico IDE non funzionerà. {new (... args)} - questo codice descrive un oggetto che dovrebbe essere una classe (potresti leggere di più sulle interfacce TS nel manuale: typescriptlang.org/docs/handbook/interfaces.html
Mark Dolbyrev

2
il problema qui è che TS ancora non ha la più pallida idea della classe modificata. Riesco a digitare secondInheritanceObj.some()e non ricevo un messaggio di avviso.
sf

Come verificare se la classe Mixed obbedisce alle interfacce?
rilut

11
Questo "nuovo approccio mixin" di Typescript sembra un ripensamento. Come sviluppatore voglio solo essere in grado di dire "Voglio che questa classe erediti ClassA e ClassB" o "Voglio che questa classe sia un mix di ClassA e ClassB" e voglio esprimerlo con una sintassi chiara che posso ricordare in 6 mesi, non quel mumbo jumbo. Se è una limitazione tecnica delle possibilità del compilatore TS, così sia, ma questa non è una soluzione.
Rui Marques

12

Purtroppo il dattiloscritto non supporta l'ereditarietà multipla. Quindi non c'è una risposta del tutto banale, probabilmente dovrai ristrutturare il tuo programma

Ecco alcuni suggerimenti:

  • Se questa classe aggiuntiva contiene un comportamento che molte delle tue sottoclassi condividono, ha senso inserirla nella gerarchia delle classi, da qualche parte in alto. Forse potresti derivare la comune superclasse di Sprite, Texture, Layer, ... da questa classe? Questa sarebbe una buona scelta, se riesci a trovare un buon posto nella gerarchia dei tipi. Ma non consiglierei di inserire questa classe in un punto casuale. L'ereditarietà esprime un "È una relazione", ad esempio un cane è un animale, una trama è un'istanza di questa classe. Dovresti chiederti se questo modella davvero la relazione tra gli oggetti nel tuo codice. Un albero di ereditarietà logica è molto prezioso

  • Se la classe aggiuntiva non si adatta logicamente alla gerarchia dei tipi, è possibile utilizzare l'aggregazione. Ciò significa che aggiungi una variabile di istanza del tipo di questa classe a una superclasse comune di Sprite, Texture, Layer, ... Quindi puoi accedere alla variabile con il suo getter / setter in tutte le sottoclassi. Questo modella un "ha una relazione".

  • Puoi anche convertire la tua classe in un'interfaccia. Quindi potresti estendere l'interfaccia con tutte le tue classi ma dovresti implementare i metodi correttamente in ogni classe. Ciò significa una certa ridondanza del codice, ma in questo caso non molto.

Devi decidere da solo quale approccio ti piace di più. Personalmente consiglierei di convertire la classe in un'interfaccia.

Un consiglio: Typescript offre proprietà, che sono zucchero sintattico per getter e setter. Potresti voler dare un'occhiata a questo: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/


2
Interessante. 1) Non posso farlo, semplicemente perché estendo PIXIe non posso cambiare la libreria per aggiungere un'altra classe sopra di essa. 2) È una delle possibili soluzioni che potrei usare, ma avrei preferito evitarla se possibile. 3) Non voglio assolutamente duplicare quel codice, potrebbe essere semplice ora, ma cosa succederà dopo? Ho lavorato a questo programma solo un giorno e ne aggiungerò molto di più in seguito, non è una buona soluzione per me. Darò un'occhiata al suggerimento, grazie per la risposta dettagliata.
Vadorequest

Link davvero interessante, ma non vedo alcun caso d'uso per risolvere il problema qui.
Vadorequest

Se tutte queste classi estendono PIXI, fai in modo che la classe ObjectThatShouldAlsoBeExtended estenda PIXI e derivi le classi Texture, Sprite, ... da questo. Questo è ciò che intendevo con l'inserimento della classe nel tipo hirarchy
lhk

PIXIdi per sé non è una classe, è un modulo, non può essere esteso, ma hai ragione, sarebbe stata un'opzione praticabile se lo fosse!
Vadorequest

1
Quindi ciascuna delle classi estende un'altra classe dal modulo PIXI? Allora hai ragione, non puoi inserire la classe ObjectThatShouldAlso nella gerarchia dei tipi. Non e possibile. Potresti ancora scegliere la relazione o l'interfaccia. Poiché desideri descrivere un comportamento comune condiviso da tutte le tue classi, ti consiglio di utilizzare un'interfaccia. È il design più pulito, anche se il codice è duplicato.
lhk

12

TypeScript supporta i decoratori e usando questa funzione più una piccola libreria chiamata typescript-mix puoi usare i mixin per avere ereditarietà multipla con solo un paio di righe

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

8

Penso che ci sia un approccio molto migliore, che consente una solida sicurezza dei tipi e scalabilità.

Prima dichiara le interfacce che vuoi implementare sulla tua classe di destinazione:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

Ora dobbiamo aggiungere l'implementazione alla Fooclasse. Possiamo usare mixin di classi che implementano anche queste interfacce:

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

Estendi la Fooclasse con i mixin di classe:

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

1
Qual è il tipo restituito dalle funzioni Bar e Bazz?
Luke Skywalker

3

Una soluzione molto complicata sarebbe quella di scorrere la classe che vuoi ereditare aggiungendo le funzioni una per una alla nuova classe genitore

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

È ora possibile accedere a tutte le proprietà di ChildAe ChildBdalla Parentclasse, tuttavia non verranno riconosciute, il che significa che riceverai avvisi comeProperty 'x' does not exist on 'typeof Parent'



0

Ci sono già tante buone risposte qui, ma voglio solo mostrare con un esempio che puoi aggiungere ulteriori funzionalità alla classe che viene estesa;

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

0

Nei design pattern esiste un principio chiamato "favorire la composizione rispetto all'ereditarietà". Dice invece di ereditare la classe B dalla classe A, inserire un'istanza della classe A all'interno della classe B come proprietà e quindi è possibile utilizzare le funzionalità della classe A all'interno della classe B. Puoi vederne alcuni esempi qui e qui .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.