Mecanografiado: ¿Cómo extender dos clases?


85

Quiero ahorrar tiempo y reutilizar código común en todas las clases que amplía las clases PIXI (una biblioteca de renderizado webGl 2d).

Interfaces de objetos:

module Game.Core {
    export interface IObject {}

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

Mi problema es que el código dentro getKeyInManagery setKeyInManagerno cambiará y quiero reutilizarlo, no duplicarlo, aquí está la implementación:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

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

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

Lo que quiero hacer es agregar automáticamente, a través de a Manager.add(), la clave utilizada en el administrador para hacer referencia al objeto dentro del propio objeto en su propiedad _keyInManager.

Entonces, tomemos un ejemplo con una textura. Aquí va elTextureManager

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

Cuando lo haga this.add(), quiero que el Game.Managers.Manager add()método llame a un método que existiría en el objeto devuelto por Game.Core.Texture.fromImage("/" + relativePath). Este objeto, en este caso sería 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 {

    }
}

Sé que IManagedObjectes una interfaz y no puede contener implementación, pero no sé qué escribir para inyectar la clase ObjectThatShouldAlsoBeExtendeddentro de mi Textureclase. Sabiendo que sería necesario el mismo proceso para Sprite, TilingSprite, Layery mucho más.

Necesito comentarios / consejos de TypeScript con experiencia aquí, debe ser posible hacerlo, pero no mediante múltiples extensiones, ya que solo es posible una en ese momento, no encontré ninguna otra solución.


7
Solo un consejo, cada vez que me encuentro con un problema de herencia múltiple, trato de recordarme a mí mismo que debo pensar en "favorecer la composición sobre la herencia" para ver si eso funciona.
burbujeo

2
Convenido. No pensaba de esa manera hace 2 años;)
Vadorequest

6
@bubbleking ¿cómo se aplicaría aquí favorecer la composición sobre la herencia?
Seanny123

Respuestas:


94

Hay una característica poco conocida en TypeScript que le permite usar Mixins para crear pequeños objetos reutilizables. Puede componerlos en objetos más grandes utilizando herencia múltiple (la herencia múltiple no está permitida para las clases, pero está permitida para los mixins, que son como interfaces con una implementación asociada).

Más información sobre Mixins de TypeScript

Creo que podrías usar esta técnica para compartir componentes comunes entre muchas clases en tu juego y reutilizar muchos de estos componentes de una sola clase en tu juego:

Aquí hay una demostración rápida de Mixins ... primero, los sabores que desea mezclar:

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

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

Luego, el método mágico para la creación de Mixin (solo lo necesita una vez en algún lugar de su programa ...)

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

Y luego puede crear clases con herencia múltiple de sabores mixtos:

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

Tenga en cuenta que no existe una implementación real en esta clase, solo lo suficiente para que pase los requisitos de las "interfaces". Pero cuando usamos esta clase, todo funciona.

var being = new Being();

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

3
Aquí está la sección Mixins en el Manual de TypeScript (pero Steve prácticamente cubrió todo lo que necesita saber en esta respuesta y en su artículo vinculado) typescriptlang.org/Handbook#mixins
Troy Gizzi

2
Typecript 2.2 ahora es compatible con Mixins
Flavien Volken

1
@FlavienVolken ¿Sabe por qué Microsoft mantuvo la antigua sección de mixins en la documentación de su manual? Por cierto, las notas de la versión son realmente difíciles de entender para un principiante en TS como yo. ¿Algún enlace para un tutorial con mixins TS 2.2+? Gracias.
David D.

4
La "forma antigua" de hacer mixins que se muestra en este ejemplo es más simple que la "forma nueva" (Typecript 2.2+). No sé por qué lo hicieron tan difícil.
tocqueville

2
La razón es que la "forma antigua" no puede obtener el tipo correctamente.
unional

25

Sugeriría usar el nuevo enfoque de mixins que se describe allí: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

Este enfoque es mejor que el enfoque "applyMixins" descrito por Fenton, porque el autocompilador le ayudaría y le mostraría todos los métodos / propiedades de las clases base y segunda herencia.

Este enfoque se puede verificar en el sitio de TS Playground .

Aquí está la implementación:

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

¿ SecondInheritanceClassNo está definido a propósito o me falta algo? Al cargar este código en el patio de juegos de TS, dice expecting =>. Finalmente, ¿podría desglosar exactamente lo que está sucediendo en la addSecondInheritancefunción, por ejemplo, cuál es el propósito new (...args)?
Seanny123

El punto principal de la implementación de tales mixins es que TODOS los métodos y propiedades de ambas clases se mostrarán en la ayuda de autocompletar IDE. Si lo desea, puede definir la segunda clase y utilizar el enfoque sugerido por Fenton, pero en este caso, el autocompletado de IDE no funcionará. {new (... args)}: este código describe un objeto que debería ser una clase (puede leer más acerca de las interfaces TS en el manual: typescriptlang.org/docs/handbook/interfaces.html
Mark Dolbyrev

2
el problema aquí es que TS todavía no tiene ni idea de la clase modificada. Puedo escribir secondInheritanceObj.some()y no recibo un mensaje de advertencia.
sf

¿Cómo comprobar si la clase Mixta obedece a las interfaces?
rilut

11
Este 'nuevo enfoque de mixins' de Typescript parece una ocurrencia tardía. Como desarrollador, solo quiero poder decir "Quiero que esta clase herede ClassA y ClassB" o "Quiero que esta clase sea una combinación de ClassA y ClassB" y quiero expresar eso con una sintaxis clara que pueda recordar. en 6 meses, no tanto tonterías. Si es una limitación técnica de las posibilidades del compilador de TS, que así sea, pero esta no es una solución.
Rui Marques

12

Lamentablemente, el mecanografiado no admite la herencia múltiple. Por lo tanto, no hay una respuesta completamente trivial, probablemente tendrá que reestructurar su programa

Aqui hay algunas sugerencias:

  • Si esta clase adicional contiene un comportamiento que comparten muchas de sus subclases, tiene sentido insertarlo en la jerarquía de clases, en algún lugar en la parte superior. ¿Quizás podrías derivar la superclase común de Sprite, Texture, Layer, ... de esta clase? Esta sería una buena opción, si puede encontrar un buen lugar en la jerarquía de tipos. Pero no recomendaría simplemente insertar esta clase en un punto aleatorio. La herencia expresa una relación "Es una -", por ejemplo, un perro es un animal, una textura es una instancia de esta clase. Tendrías que preguntarte si esto realmente modela la relación entre los objetos en tu código. Un árbol de herencia lógica es muy valioso

  • Si la clase adicional no encaja lógicamente en la jerarquía de tipos, puede utilizar la agregación. Eso significa que agrega una variable de instancia del tipo de esta clase a una superclase común de Sprite, Texture, Layer, ... Luego puede acceder a la variable con su getter / setter en todas las subclases. Esto modela un "Tiene una relación".

  • También puede convertir su clase en una interfaz. Luego, podría extender la interfaz con todas sus clases, pero tendría que implementar los métodos correctamente en cada clase. Esto significa cierta redundancia de código, pero en este caso no mucha.

Tienes que decidir por ti mismo qué enfoque te gusta más. Personalmente, recomendaría convertir la clase en una interfaz.

Un consejo: TypeScript ofrece propiedades, que son azúcar sintáctico para getters y setters. Es posible que desee echar un vistazo a esto: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/


2
Interesante. 1) No puedo hacer eso, simplemente porque extiendo PIXIy no puedo cambiar la biblioteca para agregar otra clase encima. 2) Es una de las posibles soluciones que podría usar, pero hubiera preferido evitarla si fuera posible. 3) Definitivamente no quiero duplicar ese código, puede ser simple ahora, pero ¿qué pasará después? He trabajado en este programa solo un día y agregaré mucho más más adelante, no es una buena solución para mí. Echaré un vistazo a la sugerencia, gracias por la respuesta detallada.
Vadorequest

Un enlace interesante de hecho, pero no veo ningún caso de uso para resolver el problema aquí.
Vadorequest

Si todas estas clases extienden PIXI, entonces haga que la clase ObjectThatShouldAlsoBeExtended extienda PIXI y derive las clases Texture, Sprite, ... de eso. Eso es lo que quise decir con insertar la clase en la jerarquía de tipos
lhk

PIXIen sí mismo no es una clase, es un módulo, no se puede extender, pero tienes razón, ¡habría sido una opción viable si lo fuera!
Vadorequest

1
¿Entonces cada una de las clases extiende otra clase del módulo PIXI? Entonces tiene razón, no puede insertar la clase ObjectThatShouldAlso en la jerarquía de tipos. Eso no es posible. Aún puede elegir la relación tiene-a o la interfaz. Dado que desea describir un comportamiento común que comparten todas sus clases, le recomendaría usar una interfaz. Ese es el diseño más limpio, incluso si el código está duplicado.
lhk

10

TypeScript admite decoradores , y usando esa función más una pequeña biblioteca llamada mecanografiado-mix , puede usar mixins para tener herencia múltiple con solo un par de líneas

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

7

Creo que hay un enfoque mucho mejor, que permite una escalabilidad y seguridad de tipos sólidas .

Primero declare las interfaces que desea implementar en su clase de destino:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

Ahora tenemos que agregar la implementación a la Fooclase. Podemos usar class mixins que también implemente estas interfaces:

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

Amplíe la Fooclase con los mixins de la clase:

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

¿Cuáles son los tipos devueltos de las funciones Bar y Bazz?
Luke Skywalker

3

Una solución muy intrincada sería recorrer la clase que desea heredar al agregar las funciones una por una a la nueva clase principal

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

Todas las propiedades de ChildAy ChildBahora se puede acceder desde la Parentclase, sin embargo, no se reconocerán, lo que significa que recibirá advertencias comoProperty 'x' does not exist on 'typeof Parent'


1

En los patrones de diseño existe un principio llamado "favorecer la composición sobre la herencia". Dice que en lugar de heredar la Clase B de la Clase A, coloque una instancia de la clase A dentro de la clase B como una propiedad y luego puede usar funcionalidades de la clase A dentro de la clase B. Puede ver algunos ejemplos de eso aquí y aquí .



0

Ya hay tantas buenas respuestas aquí, pero solo quiero mostrar con un ejemplo que puede agregar funcionalidad adicional a la clase que se está extendiendo;

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);
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.