Texto datilografado: Como estender duas classes?

90

Eu quero economizar meu tempo e reutilizar o código comum entre as classes que estende as classes PIXI (uma biblioteca de renderizador 2d webGl).

Interfaces de objeto:

module Game.Core {
    export interface IObject {}

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

Meu problema é que o interior de código getKeyInManagere setKeyInManagernão vai mudar e eu quero reutilizá-lo, não duplicá-lo, aqui é a implementação:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

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

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

O que quero fazer é adicionar automaticamente, por meio de a Manager.add(), a chave usada no gerenciador para fazer referência ao objeto dentro do próprio objeto em sua propriedade _keyInManager.

Então, vamos dar um exemplo com uma textura. Aqui vai oTextureManager

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 eu fizer isso this.add(), quero que o Game.Managers.Manager add()método chame um método que existiria no objeto retornado por Game.Core.Texture.fromImage("/" + relativePath). Este objeto, neste caso, seria um Texture:

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

    }
}

Eu sei que IManagedObjecté uma interface e não pode conter implementação, mas não sei o que escrever para injetar a classe ObjectThatShouldAlsoBeExtendeddentro da minha Textureclasse. Sabendo que seria necessário o mesmo processo para Sprite, TilingSprite, Layere muito mais.

Preciso de um feedback / conselhos experientes do TypeScript aqui, deve ser possível fazer isso, mas não por extensões múltiplas, pois apenas uma é possível no momento, não encontrei nenhuma outra solução.

Vadorequest
fonte
7
Apenas uma dica: sempre que me deparo com um problema de herança múltipla, tento me lembrar de pensar "prefira a composição à herança" para ver se isso vai dar certo.
bolha em
2
Acordado. Não estava pensando assim há 2 anos;)
Vadorequest
6
@bubbleking como favorecer a composição sobre a herança se aplica aqui?
Seanny123

Respostas:

97

Há um recurso pouco conhecido no TypeScript que permite usar Mixins para criar pequenos objetos reutilizáveis. Você pode compor estes em objetos maiores usando herança múltipla (herança múltipla não é permitida para classes, mas é permitida para mixins - que são como interfaces com uma implementação associada).

Mais informações sobre TypeScript Mixins

Acho que você poderia usar essa técnica para compartilhar componentes comuns entre muitas classes em seu jogo e reutilizar muitos desses componentes de uma única classe em seu jogo:

Aqui está uma rápida demonstração dos Mixins ... primeiro, os sabores que você deseja misturar:

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

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

Em seguida, o método mágico para a criação do Mixin (você só precisa disso uma vez em algum lugar do seu 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];
            }
        });
    }); 
}

E então você pode criar classes com herança múltipla de sabores mixin:

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

Observe que não há implementação real nesta classe - apenas o suficiente para fazê-la passar nos requisitos das "interfaces". Mas quando usamos esta classe - tudo funciona.

var being = new Being();

// Zzzzzzz...
being.sleep();
Fenton
fonte
3
Aqui está a seção Mixins no Manual do TypeScript (mas Steve praticamente cobriu tudo que você precisa saber nesta resposta e em seu artigo vinculado) typescriptlang.org/Handbook#mixins
Troy Gizzi
2
Typcript 2.2 agora suporta Mixins
Flavien Volken
1
@FlavienVolken Você sabe por que a Microsoft manteve a antiga seção de mixins em sua documentação de manual? A propósito, as notas de lançamento são realmente difíceis de entender para um iniciante em TS como eu. Qualquer link para um tutorial com mixins TS 2.2+? Obrigado.
David D.
4
O "jeito antigo" de fazer mixins mostrado neste exemplo é mais simples do que o "jeito novo" (Typescript 2.2+). Não sei por que tornaram isso tão difícil.
tocqueville
2
O motivo é que o "método antigo" não consegue obter o tipo correto.
unional
25

Eu sugeriria usar a nova abordagem de mixins descrita lá: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

Essa abordagem é melhor do que a abordagem "applyMixins" descrita por Fenton, porque o autocompiler o ajudaria e mostraria todos os métodos / propriedades das classes de herança base e segunda.

Essa abordagem pode ser verificada no site TS Playground .

Aqui está a implementação:

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
fonte
Não SecondInheritanceClassestá definido de propósito ou estou faltando alguma coisa? Ao carregar este código no playground do TS, ele diz expecting =>. Por fim, você poderia analisar exatamente o que está acontecendo na addSecondInheritancefunção, como qual é o propósito de new (...args)?
Seanny123
O ponto principal da implementação desses mixins é que TODOS os métodos e propriedades de ambas as classes serão mostrados na ajuda do autocomplete do IDE. Se desejar, você pode definir a 2ª classe e usar a abordagem sugerida por Fenton, mas neste caso o preenchimento automático do IDE não funcionará. {new (... args)} - este código descreve um objeto que deve ser uma classe (você pode ler mais sobre interfaces TS no manual: typescriptlang.org/docs/handbook/interfaces.html
Mark Dolbyrev
2
o problema aqui é que o TS ainda não tem idéia da classe modificada. Posso digitar secondInheritanceObj.some()e não obtenho uma mensagem de aviso.
sf
Como verificar se a classe Mixed obedece às interfaces?
rilut
11
Esta 'nova abordagem de mixins' da Typescript parece uma reflexão tardia. Como um desenvolvedor, eu só quero ser capaz de dizer "Eu quero que esta classe herde ClassA e ClassB" ou "Eu quero que esta classe seja uma mistura de ClassA e ClassB" e quero expressar isso com uma sintaxe clara que eu possa lembrar em 6 meses, não aquele jumbo mumbo. Se é uma limitação técnica das possibilidades do compilador TS que seja, mas esta não é uma solução.
Rui Marques
12

Infelizmente, o typescript não suporta herança múltipla. Portanto, não há uma resposta completamente trivial, você provavelmente terá que reestruturar seu programa

Aqui estão algumas sugestões:

  • Se essa classe adicional contém o comportamento que muitas de suas subclasses compartilham, faz sentido inseri-la na hierarquia de classes, em algum lugar no topo. Talvez você pudesse derivar a superclasse comum de Sprite, Texture, Layer, ... desta classe? Esta seria uma boa escolha, se você puder encontrar um bom local na hierarquia de tipos. Mas eu não recomendaria apenas inserir essa classe em um ponto aleatório. Herança expressa um "É um relacionamento", por exemplo, um cachorro é um animal, uma textura é uma instância desta classe. Você teria que se perguntar se isso realmente modela o relacionamento entre os objetos em seu código. Uma árvore de herança lógica é muito valiosa

  • Se a classe adicional não se encaixar logicamente na hierarquia de tipo, você pode usar a agregação. Isso significa que você adiciona uma variável de instância do tipo desta classe a uma superclasse comum de Sprite, Texture, Layer, ... Então você pode acessar a variável com seu getter / setter em todas as subclasses. Isso modela um "Tem um - relacionamento".

  • Você também pode converter sua classe em uma interface. Então, você poderia estender a interface com todas as suas classes, mas teria que implementar os métodos corretamente em cada classe. Isso significa alguma redundância de código, mas, neste caso, não muito.

Você tem que decidir por si mesmo qual abordagem você mais gosta. Pessoalmente, eu recomendaria converter a classe em uma interface.

Uma dica: o typescript oferece propriedades, que são açúcar sintático para getters e setters. Você pode querer dar uma olhada em: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/

lhk
fonte
2
Interessante. 1) Não posso fazer isso, simplesmente porque estende PIXIe não posso alterar a biblioteca para adicionar outra classe em cima dela. 2) É uma das soluções possíveis que eu poderia usar, mas teria preferido evitá-la, se possível. 3) Definitivamente, não quero duplicar esse código, pode ser simples agora, mas o que vai acontecer a seguir? Trabalhei neste programa apenas um dia e adicionarei muito mais depois, o que não é uma boa solução para mim. Vou dar uma olhada na dica, obrigado pela resposta detalhada.
Vadorequest
Link interessante mesmo, mas não vejo nenhum caso de uso para resolver o problema aqui.
Vadorequest
Se todas essas classes estendem PIXI, então apenas faça a classe ObjectThatShouldAlsoBeExtended estender PIXI e derivar as classes Texture, Sprite, ... a partir disso. Isso é o que eu quis dizer com a inserção da classe na hirarquia de tipo
lhk
PIXIem si não é uma classe, é um módulo, não pode ser estendido, mas você está certo, teria sido uma opção viável se fosse!
Vadorequest
1
Então, cada uma das classes estende outra classe do módulo PIXI? Então você está certo, você não pode inserir a classe ObjectThatShouldAlso na hierarquia de tipo. Isso não é possível. Você ainda pode escolher o relacionamento tem-um ou a interface. Já que você deseja descrever um comportamento comum que todas as suas classes compartilham, eu recomendaria usar uma interface. Esse é o design mais limpo, mesmo se o código for duplicado.
lhk
12

TypeScript suporta decoradores , e usando esse recurso mais uma pequena biblioteca chamada typescript-mix você pode usar mixins para ter herança múltipla com apenas algumas linhas

// 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
fonte
8

Acho que há uma abordagem muito melhor, que permite segurança de tipo sólida e escalabilidade.

Primeiro, declare as interfaces que deseja implementar em sua classe de destino:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

Agora temos que adicionar a implementação à Fooclasse. Podemos usar mixins de classes que também implementam essas 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!");
    }
  };
}

Estenda a Fooaula com os mixins de aula:

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
fonte
1
Quais são os tipos retornados das funções Bar e Bazz?
Luke Skywalker
3

Uma solução muito hacky seria fazer um loop pela classe que você deseja herdar ao adicionar as funções uma por uma à nova classe pai

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 as propriedades de ChildAe ChildBagora podem ser acessadas a partir da Parentclasse, no entanto, elas não serão reconhecidas, o que significa que você receberá avisos comoProperty 'x' does not exist on 'typeof Parent'

WillCooter
fonte
0

Já existem tantas respostas boas aqui, mas eu só quero mostrar com um exemplo que você pode adicionar funcionalidade adicional à classe que está sendo estendida;

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
fonte
0

Nos padrões de projeto, existe um princípio denominado "favorecer a composição em vez da herança". Diz em vez de herdar a Classe B da Classe A, coloque uma instância da classe A dentro da classe B como uma propriedade e então você pode usar as funcionalidades da classe A dentro da classe B. Você pode ver alguns exemplos disso aqui e aqui .

AmirHossein Rezaei
fonte