Машинопись: как расширить два класса?

90

Я хочу сэкономить свое время и повторно использовать общий код для разных классов, который расширяет классы PIXI (2d-библиотека рендеринга webGl).

Объектные интерфейсы:

module Game.Core {
    export interface IObject {}

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

Моя проблема в том, что код внутри getKeyInManagerи setKeyInManagerне изменится, и я хочу использовать его повторно, а не дублировать, вот реализация:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

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

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

Я хочу автоматически добавить через a Manager.add()ключ, используемый в диспетчере для ссылки на объект внутри самого объекта в его свойстве _keyInManager.

Итак, возьмем пример с текстурой. Вот идет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);
        }
    }
}

Когда я это сделаю this.add(), я хочу, чтобы Game.Managers.Manager add()метод вызывал метод, который существовал бы для объекта, возвращаемого Game.Core.Texture.fromImage("/" + relativePath). Этот объект в данном случае будет Texture:

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

    }
}

Я знаю, что IManagedObjectэто интерфейс и не может содержать реализацию, но я не знаю, что написать, чтобы внедрить класс ObjectThatShouldAlsoBeExtendedвнутри моего Textureкласса. Зная , что тот же процесс будет необходим для Sprite, TilingSprite, Layerи многих других.

Мне нужны опытные отзывы / советы по TypeScript, это должно быть возможно, но не с помощью нескольких расширений, поскольку в то время возможно только одно, другого решения я не нашел.

Vadorequest
источник
7
Просто совет: всякий раз, когда я сталкиваюсь с проблемой множественного наследования, я пытаюсь напомнить себе, что нужно думать «предпочитать композицию наследованию», чтобы посмотреть, справится ли это с работой.
Bubbleking
2
Согласовано. Не думал так 2 года назад;)
Vadorequest
6
@bubbleking, как здесь будет применяться предпочтение композиции перед наследованием?
Seanny123

Ответы:

97

В TypeScript есть малоизвестная функция, позволяющая использовать миксины для создания повторно используемых небольших объектов. Вы можете скомпоновать их в более крупные объекты, используя множественное наследование (множественное наследование не разрешено для классов, но разрешено для миксинов, которые похожи на интерфейсы со связанной реализацией).

Дополнительная информация о миксинах TypeScript

Я думаю, вы могли бы использовать эту технику, чтобы разделять общие компоненты между многими классами в вашей игре и повторно использовать многие из этих компонентов из одного класса в вашей игре:

Вот быстрая демонстрация Mixins ... во-первых, вкусы, которые вы хотите смешать:

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

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

Затем волшебный метод создания миксинов (вам понадобится только один раз где-нибудь в вашей программе ...)

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 Being implements CanEat, CanSleep {
        eat: () => void;
        sleep: () => void;
}
applyMixins (Being, [CanEat, CanSleep]);

Обратите внимание, что в этом классе нет реальной реализации - достаточно, чтобы он соответствовал требованиям «интерфейсов». Но когда мы используем этот класс - все работает.

var being = new Being();

// Zzzzzzz...
being.sleep();
Фентон
источник
3
Вот раздел Mixins в Руководстве по TypeScript (но Стив в значительной степени охватил все, что вам нужно знать в этом ответе и в своей связанной статье) typescriptlang.org/Handbook#mixins
Troy Gizzi
2
Машинопись 2.2 теперь поддерживает Примеси
Flavien Volken
1
@FlavienVolken Знаете ли вы, почему Microsoft сохранила раздел старых миксинов в своей справочной документации? Между прочим, новичку в TS вроде меня действительно сложно понять примечания к выпуску. Есть ли ссылка на руководство по миксинам TS 2.2+? Благодарю.
Дэвид Д.
4
«Старый способ» создания миксинов, показанный в этом примере, проще, чем «новый способ» (Typescript 2.2+). Не знаю, почему они сделали это так сложно.
tocqueville
2
Причина в том, что "старый способ" не может правильно получить тип.
unional
25

Я бы предложил использовать описанный там новый подход миксинов: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

Этот подход лучше, чем подход «applyMixins», описанный Фентоном, потому что автокомпилятор поможет вам и покажет все методы / свойства как из базового, так и из второго классов наследования.

Этот подход можно проверить на сайте TS Playground. .

Вот реализация:

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();
Марк Долбырев
источник
Разве SecondInheritanceClassне определено на цели или я что - то отсутствует? При загрузке этого кода на игровую площадку TS он говорит expecting =>. Наконец, не могли бы вы точно рассказать, что происходит в addSecondInheritanceфункции, например, для чего new (...args)?
Seanny123,
Суть реализации таких миксинов в том, что ВСЕ методы и свойства обоих классов будут показаны в автозаполнении справки IDE. Если хотите, вы можете определить второй класс и использовать подход, предложенный Фентоном, но в этом случае автозаполнение IDE не будет работать. {new (... args)} - этот код описывает объект, который должен быть классом (подробнее об интерфейсах TS можно прочитать в справочнике: typescriptlang.org/docs/handbook/interfaces.html
Марк Долбырев
2
проблема здесь в том, что TS по-прежнему не имеет представления о модифицированном классе. Я могу печатать secondInheritanceObj.some()и не получаю предупреждающих сообщений.
SF
Как проверить, подчиняется ли смешанный класс интерфейсам?
rilut
11
Этот «новый подход к миксинам» от Typescript выглядит второстепенным. Как разработчик я просто хочу иметь возможность сказать «Я хочу, чтобы этот класс унаследовал ClassA и ClassB» или «Я хочу, чтобы этот класс был смесью ClassA и ClassB», и я хочу выразить это с помощью четкого синтаксиса, который я могу запомнить. через 6 месяцев, а не то лабиринт. Если это техническое ограничение возможностей компилятора TS, пусть так, но это не решение.
Руи Маркес
12

К сожалению, машинописный текст не поддерживает множественное наследование. Следовательно, полностью тривиального ответа нет, вам, вероятно, придется реструктурировать свою программу.

Вот несколько предложений:

  • Если этот дополнительный класс содержит поведение, которое разделяют многие из ваших подклассов, имеет смысл вставить его в иерархию классов где-нибудь наверху. Может быть, вы могли бы получить общий суперкласс Sprite, Texture, Layer, ... из этого класса? Это будет хороший выбор, если вы сможете найти хорошее место в типе hirarchy. Но я бы не рекомендовал просто вставлять этот класс в случайном месте. Наследование выражает «есть - отношение», например, собака - это животное, текстура - это экземпляр этого класса. Вы должны спросить себя, действительно ли это моделирует отношения между объектами в вашем коде. Дерево логического наследования очень ценно

  • Если дополнительный класс логически не вписывается в иерархию типов, вы можете использовать агрегирование. Это означает, что вы добавляете переменную экземпляра типа этого класса к общему суперклассу Sprite, Texture, Layer, ... Затем вы можете получить доступ к переменной с помощью ее получателя / установщика во всех подклассах. Это моделирует «Имеет - отношения».

  • Вы также можете преобразовать свой класс в интерфейс. Тогда вы могли бы расширить интерфейс со всеми своими классами, но должны были бы правильно реализовать методы в каждом классе. Это означает некоторую избыточность кода, но в данном случае небольшую.

Вы должны решить для себя, какой подход вам больше нравится. Лично я бы рекомендовал преобразовать класс в интерфейс.

Один совет: Typescript предлагает свойства, которые являются синтаксическим сахаром для геттеров и сеттеров. Возможно, вы захотите взглянуть на это: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/

lhk
источник
2
Интересно. 1) Я не могу этого сделать, просто потому что я расширяюсь, PIXIи я не могу изменить библиотеку, чтобы добавить еще один класс поверх нее. 2) Это одно из возможных решений, которое я мог бы использовать, но я бы предпочел избегать его, если это возможно. 3) Я категорически не хочу дублировать этот код, теперь это может быть просто, но что будет дальше? Я работал над этой программой всего день, а позже добавлю еще много, не очень хорошее решение для меня. Я посмотрю на подсказку, спасибо за подробный ответ.
Vadorequest
Действительно интересная ссылка, но я не вижу здесь варианта использования для решения проблемы.
Vadorequest
Если все эти классы расширяют PIXI, просто сделайте так, чтобы класс ObjectThatShouldAlsoBeExtended расширял PIXI и извлекал из него классы Texture, Sprite, ... Вот что я имел в виду, вставив класс в тип hirarchy
lhk
PIXIсам по себе не класс, это модуль, его нельзя расширять, но вы правы, это было бы жизнеспособным вариантом, если бы так было!
Vadorequest
1
Итак, каждый из классов расширяет другой класс из модуля PIXI? Тогда вы правы, вы не можете вставить класс ObjectThatShouldAlso в тип hirarchy. Это невозможно. Вы по-прежнему можете выбрать отношение «есть-а» или интерфейс. Поскольку вы хотите описать общее поведение всех ваших классов, я бы рекомендовал использовать интерфейс. Это самый чистый дизайн, даже если код дублируется.
lhk
12

TypeScript поддерживает декораторы , и, используя эту функцию плюс небольшую библиотеку под названием typescript-mix, вы можете использовать миксины для множественного наследования всего с парой строк.

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

Я думаю, что есть гораздо лучший подход, который обеспечивает надежную безопасность типов и масштабируемость.

Сначала объявите интерфейсы, которые вы хотите реализовать в своем целевом классе:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

Теперь нам нужно добавить реализацию в Fooкласс. Мы можем использовать миксины классов, которые также реализуют эти интерфейсы:

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

Расширьте Fooкласс с помощью миксинов класса:

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
Какой тип возвращают функции Bar и Bazz?
Люк Скайуокер
3

Очень хакерским решением было бы перебрать класс, который вы хотите унаследовать, добавляя одну за другой функции к новому родительскому классу.

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

Все свойства ChildAи ChildBтеперь доступны из Parentкласса, однако они не будут распознаны, что означает, что вы получите такие предупреждения, какProperty 'x' does not exist on 'typeof Parent'

WillCooter
источник
0

Здесь уже так много хороших ответов, но я просто хочу показать на примере, что вы можете добавить дополнительную функциональность в расширяемый класс;

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

В паттернах проектирования есть принцип, называемый «предпочтение композиции перед наследованием». В нем говорится, что вместо наследования класса B от класса A поместите экземпляр класса A внутри класса B как свойство, а затем вы можете использовать функции класса A внутри класса B. Вы можете увидеть некоторые примеры этого здесь и здесь .

Амир Хосейн Резаи
источник