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


85

Я хочу заощадити свій час і використати загальний код для класів, який розширює класи 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, це має бути можливо зробити, але не кількома розширеннями, оскільки на той момент можливо лише одне, я не знайшов іншого рішення.


7
Тільки підказка, щоразу, коли я стикаюся з проблемою багаторазового успадкування, я намагаюся нагадати собі думати "віддавати перевагу композиції над спадщиною", щоб перевірити, чи це допоможе.
барботування

2
Домовились. Не думав так 2 роки тому;)
Vadorequest

6
@bubbleking, як тут застосовуватиметься перевага композиції над спадщиною?
Seanny123

Відповіді:


94

У TypeScript є мало відома функція, яка дозволяє використовувати Mixins для створення повторно використовуваних малих об’єктів. Ви можете скомпонувати їх у більші об’єкти, використовуючи багаторазове успадкування (множинне успадкування не дозволяється для класів, але це дозволено для мікшинів - подібних до інтерфейсів із пов’язаною реалізацією).

Докладніше про Mixins Mixins

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

Ось коротка демонстрація Mixins ... по-перше, смаки, які ви хочете змішати:

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

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

Потім магічний метод для створення Mixin (це потрібно лише один раз десь у вашій програмі ...)

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
Ось розділ Домішки в машинопису Handbook (але Стів досить багато покриті всі , що вам потрібно знати в цій відповіді, і в його пов'язаних статей) typescriptlang.org/Handbook#mixins
Трой Gizzi

2
Машинопис 2.2 тепер підтримує Mixins
Flavien Volken

1
@FlavienVolken Чи знаєте ви, чому корпорація Майкрософт зберегла старий розділ мікшинів у документації довідника? До речі, примітки до випусків дійсно важко зрозуміти для початківця в TS, як я. Будь-яке посилання для підручника з мікшинами TS 2.2+? Дякую.
Девід Д.

4
Показаний у цьому прикладі "старий спосіб" робити міксини простіший, ніж "новий спосіб" (Typescript 2.2+). Не знаю, чому вони так ускладнили.
Токвіль

2
Причина в тому, що "по-старому" не вдається правильно отримати тип.
профспілка

25

Я б запропонував використовувати новий підхід до мікшинів, описаний там: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

Цей підхід кращий, ніж підхід "applyMixins", описаний Фентоном, оскільки автокомпілятор допоможе вам і покаже всі методи / властивості як базового, так і 2-го класів успадкування.

Цей підхід можна перевірити на сайті 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. Якщо ви хочете, ви можете визначити 2-й клас і скористатися підходом, запропонованим Fenton, але в цьому випадку автозаповнення IDE не буде працювати. {Новий (... арг)} - це код описує об'єкт , який повинен бути класом (ви можете прочитати про інтерфейси TS більш в керівництві: typescriptlang.org/docs/handbook/interfaces.html
Марк Dolbyrev

2
проблема тут полягає в тому, що TS все ще не знає про змінений клас. Я можу ввести текст secondInheritanceObj.some()і не отримати попереджувальне повідомлення.
sf

Як перевірити, чи сумісний клас підпорядковується інтерфейсам?
rilut

11
Цей `` новий підхід до змішування '' від Typescript виглядає як задум. Як розробник я просто хочу мати можливість сказати "Я хочу, щоб цей клас успадкував ClassA і ClassB" або "Я хочу, щоб цей клас був сумішшю ClassA і ClassB", і я хочу висловити це чітким синтаксисом, який я пам'ятаю через 6 місяців, не те, що мамбо. Якщо це технічне обмеження можливостей компілятора TS, нехай це буде, але це не рішення.
Rui Marques

12

На жаль, машинопис не підтримує множинне успадкування. Тому немає абсолютно тривіальної відповіді, вам, мабуть, доведеться перебудувати свою програму

Ось кілька порад:

  • Якщо цей додатковий клас містить поведінку, якою користуються багато хто з ваших підкласів, має сенс вставити його в ієрархію класів, десь вгорі. Можливо, ви могли б отримати загальний суперклас Sprite, Texture, Layer, ... з цього класу? Це було б непоганим вибором, якщо ви знайдете гарне місце у типі ірархії. Але я б не рекомендував просто вставляти цей клас у випадковій точці. Спадщина виражає "відносини - це", наприклад, собака - тварина, текстура є прикладом цього класу. Вам доведеться запитати себе, чи справді це моделює взаємозв'язок між об'єктами у вашому коді. Логічне дерево успадкування є дуже цінним

  • Якщо додатковий клас логічно не вписується в ієрархію типів, ви можете використовувати агрегацію. Це означає, що ви додаєте змінну екземпляра типу цього класу до загального суперкласу Sprite, Texture, Layer, ... Тоді ви можете отримати доступ до змінної за допомогою її getter / setter у всіх підкласах. Це модель "має стосунки".

  • Ви також можете перетворити свій клас на інтерфейс. Тоді ви можете розширити інтерфейс з усіма своїми класами, але вам доведеться правильно застосовувати методи в кожному класі. Це означає деяку надмірність коду, але в цьому випадку не дуже.

Ви повинні самі вирішити, який підхід вам більше подобається. Особисто я порекомендував би перетворити клас на інтерфейс.

Одна порада: машинопис пропонує властивості, які є синтаксичним цукром для геттерів та сетерів. Можливо, ви захочете поглянути на це: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/


2
Цікаво. 1) Я не можу цього зробити, просто тому, що я розширюю PIXIі я не можу змінити бібліотеку, щоб додати ще один клас поверх неї. 2) Це одне з можливих рішень, яке я міг би використати, але я б вважав за краще уникати його, якщо це можливо. 3) Я остаточно не хочу дублювати цей код, це може бути зараз просто, але що буде далі? Я працював над цією програмою лише день, і пізніше додаю набагато більше, що для мене не найкраще рішення. Погляну підказку, дякую за детальну відповідь.
Vadorequest

Дійсно цікаве посилання, але я не бачу тут жодного варіанту вирішення проблеми.
Vadorequest

Якщо всі ці класи розширюють PIXI, тоді просто зробіть клас ObjectThatShouldAlsoBeExtended розширенням PIXI і виведіть з цього класи Texture, Sprite, .... Ось що я мав на увазі, вставляючи клас у тип hirarchy
lhk

PIXIсам по собі не клас, це модуль, його неможливо розширити, але ви маєте рацію, це було б життєздатним варіантом, якби це було!
Vadorequest

1
Отже, кожен із класів поширює ще один клас із модуля PIXI? Тоді ви маєте рацію, ви не можете вставити клас ObjectThatShouldAlso у тип hirarchy. Це неможливо. Ви все ще можете вибрати взаємозв'язок між інтерфейсом або інтерфейсом. Оскільки ви хочете описати загальну поведінку, якою поділяються всі ваші класи, я б рекомендував використовувати інтерфейс. Це найчистіший дизайн, навіть якщо код дублюється.
lhk

10

TypeScript підтримує декоратори , і, використовуючи цю функцію, а також невелику бібліотеку, що називається typecript-mix, ви можете використовувати mixins, щоб мати багаторазове успадкування лише за пару рядків

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

Я думаю, що є набагато кращий підхід, який забезпечує надійну безпеку типу та масштабованість.

Спочатку оголосіть інтерфейси, які ви хочете реалізувати у своєму цільовому класі:

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

Що таке тип функцій 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'


1

У шаблонах дизайну існує принцип, який називається "надання переваги композиції над спадщиною". У ньому сказано, що замість успадкування класу B від класу A, помістіть екземпляр класу A всередину класу B як властивість, і тоді ви можете використовувати функціональні можливості класу A всередині класу B. Деякі приклади цього ви можете побачити тут і тут .



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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.