Maszynopis: Jak rozszerzyć dwie klasy?

85

Chcę zaoszczędzić czas i ponownie używać wspólnego kodu między klasami, który rozszerza klasy PIXI (biblioteka renderująca 2d webGl).

Interfejsy obiektów:

module Game.Core {
    export interface IObject {}

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

Moim problemem jest to, że kod wewnątrz getKeyInManageri setKeyInManagernie ulegnie zmianie i chcę go ponownie wykorzystać, a nie powielać, oto implementacja:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

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

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

Chcę automatycznie dodać Manager.add()klucz używany w menedżerze do odniesienia się do obiektu wewnątrz samego obiektu w jego właściwościach _keyInManager.

Weźmy więc przykład z teksturą. Tutaj idzieTextureManager

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

Kiedy to zrobię this.add(), chcę, aby Game.Managers.Manager add()metoda wywoływała metodę, która istniałaby w obiekcie zwróconym przez Game.Core.Texture.fromImage("/" + relativePath). Tym obiektem w tym przypadku byłby Texture:

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

    }
}

Wiem, że IManagedObjectjest to interfejs i nie może zawierać implementacji, ale nie wiem, co napisać, aby wstrzyknąć klasę ObjectThatShouldAlsoBeExtendeddo mojej Textureklasy. Wiedząc, że sam proces będzie wymagana Sprite, TilingSprite, Layeri więcej.

Potrzebuję tutaj opinii / porad doświadczonych w języku TypeScript, musi to być możliwe, ale nie przez wiele rozszerzeń, ponieważ w tym momencie możliwy jest tylko jeden, nie znalazłem żadnego innego rozwiązania.

Vadorequest
źródło
7
Wskazówka: ilekroć napotykam problem wielokrotnego dziedziczenia, staram się przypomnieć sobie, żebym pomyślał „przedkładaj kompozycję nad dziedziczenie”, aby sprawdzić, czy to zadziała.
Bubbleking
2
Zgoda. Nie myślałem w ten sposób 2 lata temu;)
Vadorequest
6
@bubbleking. Jakie zastosowanie ma tutaj przedkładanie kompozycji nad dziedziczenie?
Seanny123

Odpowiedzi:

94

W języku TypeScript jest mało znana funkcja, która umożliwia używanie mikserów do tworzenia małych obiektów wielokrotnego użytku. Możesz komponować je w większe obiekty przy użyciu wielokrotnego dziedziczenia (wielokrotne dziedziczenie nie jest dozwolone w przypadku klas, ale jest dozwolone w przypadku miksów - które są jak interfejsy z powiązaną implementacją).

Więcej informacji o mieszankach TypeScript

Myślę, że możesz użyć tej techniki, aby udostępnić wspólne komponenty wielu klasom w twojej grze i ponownie użyć wielu z tych komponentów z jednej klasy w twojej grze:

Oto szybkie demo Mixins ... najpierw smaki, które chcesz mieszać:

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

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

Następnie magiczna metoda tworzenia Mixinów (potrzebujesz tego tylko raz gdzieś w swoim programie ...)

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

Następnie możesz tworzyć klasy z wielokrotnym dziedziczeniem po mieszanych smakach:

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

Zauważ, że nie ma rzeczywistej implementacji w tej klasie - wystarczy, aby spełniała wymagania "interfejsów". Ale kiedy używamy tej klasy - wszystko działa.

var being = new Being();

// Zzzzzzz...
being.sleep();
Fenton
źródło
3
Oto sekcja Mixins w Podręczniku języka TypeScript (ale Steve prawie omówił wszystko, co musisz wiedzieć w tej odpowiedzi oraz w swoim powiązanym artykule) typescriptlang.org/Handbook#mixins
Troy Gizzi
2
Typescript 2.2 obsługuje
Flavien Volken
1
@FlavienVolken Czy wiesz, dlaczego firma Microsoft zachowała starą sekcję dotyczącą miksów w dokumentacji podręcznika? Nawiasem mówiąc, notatki wydawnicze są naprawdę trudne do zrozumienia dla początkującego w TS, takiego jak ja. Czy jest jakiś link do samouczka z mikserami TS 2.2+? Dzięki.
David D.
4
„Stary sposób” wykonywania miksów pokazany w tym przykładzie jest prostszy niż „nowy sposób” (Typescript 2.2+). Nie wiem, dlaczego tak to utrudnili.
tocqueville
2
Powodem jest to, że „stary sposób” nie może poprawnie uzyskać typu.
unional
25

Proponuję skorzystać z opisanego tam nowego podejścia do miksów: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

To podejście jest lepsze niż podejście „applyMixins” opisane przez Fentona, ponieważ autokompilator pomoże ci i pokaże wszystkie metody / właściwości z podstawowej i drugiej klasy dziedziczenia.

To podejście można sprawdzić w witrynie TS Playground .

Oto realizacja:

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
źródło
Jest SecondInheritanceClassniezdefiniowane celowo albo ja czegoś brakuje? Podczas ładowania tego kodu do placu zabaw TS, mówi expecting =>. Na koniec, czy możesz dokładnie określić, co dzieje się w addSecondInheritancefunkcji, na przykład jaki jest jej cel new (...args)?
Seanny123
Głównym celem takiej implementacji mixinów jest to, że WSZYSTKIE metody i właściwości obu klas zostaną pokazane w pomocy do autouzupełniania IDE. Jeśli chcesz, możesz zdefiniować drugą klasę i użyć podejścia sugerowanego przez Fentona, ale w tym przypadku autouzupełnianie IDE nie zadziała. {nowy (... args)} - kod ten opisuje przedmiot, który powinien być klasą (można przeczytać o interfejsach TS więcej w podręczniku: typescriptlang.org/docs/handbook/interfaces.html
Mark Dolbyrev
2
Problem polega na tym, że TS nadal nie ma pojęcia o zmodyfikowanej klasie. Potrafię pisać secondInheritanceObj.some()i nie otrzymuję komunikatu ostrzegawczego.
sf
Jak sprawdzić, czy klasa Mixed jest zgodna z interfejsami?
rilut
11
To „nowe podejście do miksów” z Typescript wygląda na refleksję. Jako programista chcę po prostu móc powiedzieć „Chcę, aby ta klasa odziedziczyła ClassA i ClassB” lub „Chcę, aby ta klasa była mieszanką ClassA i ClassB” i chcę to wyrazić za pomocą jasnej składni, którą pamiętam za 6 miesięcy, a nie ten mumbo jumbo. Jeśli jest to techniczne ograniczenie możliwości kompilatora TS, niech tak będzie, ale to nie jest rozwiązanie.
Rui Marques,
12

Niestety maszynopis nie obsługuje wielokrotnego dziedziczenia. Dlatego nie ma całkowicie trywialnej odpowiedzi, prawdopodobnie będziesz musiał przebudować swój program

Oto kilka sugestii:

  • Jeśli ta dodatkowa klasa zawiera zachowanie wspólne dla wielu twoich podklas, sensowne jest umieszczenie jej w hierarchii klas, gdzieś na górze. Może mógłbyś wyprowadzić wspólną superklasę Sprite, Texture, Layer, ... z tej klasy? Byłby to dobry wybór, jeśli możesz znaleźć dobre miejsce w hirarchii typów. Ale nie polecałbym po prostu wstawiać tej klasy w przypadkowym miejscu. Dziedziczenie wyraża „Jest - zależnością”, np. Pies jest zwierzęciem, tekstura jest przykładem tej klasy. Należy zadać sobie pytanie, czy to naprawdę modeluje relacje między obiektami w kodzie. Logiczne drzewo dziedziczenia jest bardzo cenne

  • Jeśli dodatkowa klasa nie pasuje logicznie do hierarchii typów, można użyć agregacji. Oznacza to, że dodajesz zmienną instancji typu tej klasy do wspólnej nadklasy Sprite, Texture, Layer, ... Następnie możesz uzyskać dostęp do zmiennej za pomocą jej metody pobierającej / ustawiającej we wszystkich podklasach. To modeluje „ma - związek”.

  • Możesz także przekształcić swoją klasę w interfejs. Następnie możesz rozszerzyć interfejs o wszystkie swoje klasy, ale musiałbyś poprawnie zaimplementować metody w każdej klasie. Oznacza to pewną redundancję kodu, ale w tym przypadku niewiele.

Musisz sam zdecydować, które podejście lubisz najbardziej. Osobiście poleciłbym przekonwertować klasę na interfejs.

Jedna wskazówka: Typescript oferuje właściwości, które są cukrem syntaktycznym dla metod pobierających i ustawiających. Możesz rzucić okiem na to: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/

lhk
źródło
2
Ciekawy. 1) Nie mogę tego zrobić, po prostu dlatego, że rozszerzam PIXIi nie mogę zmienić biblioteki, aby dodać do niej kolejną klasę. 2) Jest to jedno z możliwych rozwiązań, których mógłbym użyć, ale wolałbym go unikać, jeśli to możliwe. 3) Zdecydowanie nie chcę powielać tego kodu, teraz może to być proste, ale co będzie dalej? Pracowałem nad tym programem tylko jeden dzień i później dodam dużo więcej, co nie jest dla mnie dobrym rozwiązaniem. Rzucę okiem na wskazówkę, dzięki za szczegółową odpowiedź.
Vadorequest
Rzeczywiście ciekawy link, ale nie widzę tutaj żadnego przypadku użycia do rozwiązania problemu.
Vadorequest
Jeśli wszystkie te klasy rozszerzają PIXI, po prostu spraw, aby klasa ObjectThatShouldAlsoBeExtended rozszerzała PIXI i wyprowadzała z tego klasy Texture, Sprite, .... To właśnie miałem na myśli, wstawiając klasę do
hirarchii
PIXISamo w sobie nie jest klasą, jest modułem, nie można go rozszerzyć, ale masz rację, to byłaby realna opcja, gdyby była!
Vadorequest
1
Czyli każda z klas rozszerza inną klasę z modułu PIXI? W takim razie masz rację, nie możesz wstawić klasy ObjectThatShouldAlso do hierarchii typów. To nie jest możliwe. Nadal możesz wybrać powiązanie lub interfejs. Ponieważ chcesz opisać typowe zachowanie wszystkich twoich klas, polecam użycie interfejsu. To najczystszy projekt, nawet jeśli kod jest zduplikowany.
lhk
10

TypeScript obsługuje dekoratory i używając tej funkcji oraz małej biblioteki zwanej mieszanką maszynopisu , możesz użyć kombinacji, aby mieć wielokrotne dziedziczenie za pomocą zaledwie kilku 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;
}
Ivan Castellanos
źródło
7

Myślę, że jest znacznie lepsze podejście, które pozwala na solidne bezpieczeństwo typów i skalowalność.

Najpierw zadeklaruj interfejsy, które chcesz zaimplementować w swojej klasie docelowej:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

Teraz musimy dodać implementację do Fooklasy. Możemy użyć mixinów klas, które również implementują następujące interfejsy:

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

Rozszerz Fooklasę o mieszanki klas:

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
źródło
Jaki jest zwracany typ funkcji Bar i Bazz?
Luke Skywalker
3

Bardzo zmyślnym rozwiązaniem byłoby przechodzenie przez klasę, którą chcesz odziedziczyć, po dodaniu funkcji jedna po drugiej do nowej klasy nadrzędnej

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

Wszystkie właściwości ChildAi ChildBsą teraz dostępne z poziomu Parentklasy, jednak nie zostaną rozpoznane, co oznacza, że ​​otrzymasz ostrzeżenia, takie jakProperty 'x' does not exist on 'typeof Parent'

WillCooter
źródło
1

We wzorcach projektowych obowiązuje zasada zwana „przedkładaniem kompozycji nad dziedziczenie”. Mówi, że zamiast dziedziczyć klasę B z klasy A, umieść instancję klasy A w klasie B jako właściwość, a następnie możesz użyć funkcji klasy A w klasie B. Możesz zobaczyć kilka przykładów tutaj i tutaj .

AmirHossein Rezaei
źródło
0

Jest tu już tak wiele dobrych odpowiedzi, ale chcę tylko pokazać na przykładzie, że można dodać dodatkowe funkcje do rozszerzanej klasy;

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
źródło