Typescript:2つのクラスを拡張する方法は?


90

時間を節約し、PIXIクラス(2d webGlレンダラーライブラリ)を拡張するクラス間で共通のコードを再利用したいと思います。

オブジェクトインターフェイス:

module Game.Core {
    export interface IObject {}

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

私の問題は、コード内のことであるgetKeyInManagersetKeyInManager変更されませんし、私はないそれを複製し、それを再利用したい、ここでの実装は次のとおりです。

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

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

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

私がやりたいの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。同じプロセスをするために必要とされるであろうことを知ってSpriteTilingSpriteLayerおよびより。

ここには経験豊富なTypeScriptフィードバック/アドバイスが必要です。それは可能である必要がありますが、一度に1つしか不可能であるため、複数の拡張ではなく、他の解決策は見つかりませんでした。


7
ヒントとして、多重継承の問題に遭遇したときはいつでも、「継承よりも構成を優先する」ことを考えて、それでうまくいくかどうかを確認しようとしています。
2017年

2
同意しました。2年前はそのように考えていませんでした;)
Vadorequest 2017年

6
@bubbleking継承よりもコンポジションを優先することは、ここでどのように適用されますか?
Seanny123 2017

回答:


97

TypeScriptには、ミックスインを使用して再利用可能な小さなオブジェクトを作成できる、あまり知られていない機能があります。多重継承を使用して、これらをより大きなオブジェクトに構成できます(クラスでは多重継承は許可されていませんが、ミックスインでは許可されています。これは、関連する実装を持つインターフェイスのようなものです)。

TypeScriptミックスインの詳細

この手法を使用して、ゲーム内の多くのクラス間で共通のコンポーネントを共有し、ゲーム内の1つのクラスからこれらのコンポーネントの多くを再利用できると思います。

これが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
TypeScriptハンドブックのMixinsセクションは次のとおりです(ただし、Steveは、この回答とリンクされた記事で知っておく必要のあることをほぼすべてカバーしています)typescriptlang.org/Handbook#mixins
Troy Gizzi 2015

2
Typescript 2.2が
Mixinsを

1
@FlavienVolken Microsoftがハンドブックのドキュメントに古いミックスインセクションを残した理由を知っていますか?ちなみに、私のようなTSの初心者にとって、リリースノートは本当に理解しにくいです。TS 2.2以降のミックスインを使用したチュートリアルへのリンクはありますか?ありがとう。
David D.

4
この例に示されているミックスインを実行する「古い方法」は、「新しい方法」(Typescript 2.2以降)よりも簡単です。なぜそんなに難しくなったのかわかりません。
tocqueville 2017

2
その理由は、「古い方法」ではタイプを正しく取得できないためです。
unional 2017

25

そこで説明されている新しいミックスインアプローチを使用することをお勧めしますhttps//blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

このアプローチは、Fentonによって説明されている「applyMixins」アプローチよりも優れています。オートコンパイラが、基本クラスと2番目の継承クラスの両方からのすべてのメソッド/プロパティを表示するのに役立つからです。

このアプローチは、TSPlaygroundサイトで確認できます。

実装は次のとおりです。

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 2017

両方のクラスのすべてのメソッドとプロパティがオートコンプリートIDEヘルプに表示される、このようなミックスイン実装の要点。必要に応じて、2番目のクラスを定義し、Fentonが提案するアプローチを使用できますが、この場合、IDEオートコンプリートは機能しません。{new(... args)}-このコードは、クラスである必要があるオブジェクトを記述します(TSインターフェイスについてはハンドブックで詳しく読むことができます:typescriptlang.org/docs/handbook/interfaces.html
Mark Dolbyrev 2017

2
ここでの問題は、TSが変更されたクラスについての手がかりをまだ持っていないことです。入力できsecondInheritanceObj.some()ますが、警告メッセージは表示されません。
sf 2018

混合クラスがインターフェースに従うかどうかを確認する方法は?
rilut 2018

11
Typescriptからのこの「新しいミックスインアプローチ」は後付けのように見えます。開発者として、「このクラスにClassAとClassBを継承させたい」または「このクラスをClassAとClassBのミックスインにしたい」と言いたいだけで、覚えやすい明確な構文で表現したいと思います。 6か月で、その巨大なジャンボではありません。TSコンパイラの可能性の技術的な制限である場合はそうですが、これは解決策ではありません。
ルイ・マルケス

12

残念ながら、typescriptは多重継承をサポートしていません。したがって、完全に些細な答えはありません。おそらくプログラムを再構築する必要があります。

ここにいくつかの提案があります:

  • この追加のクラスに、多くのサブクラスが共有する動作が含まれている場合は、それをクラス階層の最上位のどこかに挿入するのが理にかなっています。たぶん、このクラスからスプライト、テクスチャ、レイヤーなどの一般的なスーパークラスを派生させることができますか?hirarchyタイプで良い場所を見つけることができれば、これは良い選択でしょう。ただし、このクラスをランダムな場所に挿入することはお勧めしません。継承は「Isa-関係」を表します。たとえば、犬は動物であり、テクスチャはこのクラスのインスタンスです。これが実際にコード内のオブジェクト間の関係をモデル化しているかどうかを自問する必要があります。論理継承ツリーは非常に価値があります

  • 追加のクラスが型階層に論理的に適合しない場合は、集約を使用できます。つまり、このクラスのタイプのインスタンス変数を、スプライト、テクスチャ、レイヤーなどの共通のスーパークラスに追加します。その後、すべてのサブクラスのゲッター/セッターを使用して変数にアクセスできます。これは「Hasa-relationship」をモデル化したものです。

  • クラスをインターフェースに変換することもできます。次に、すべてのクラスでインターフェイスを拡張できますが、各クラスでメソッドを正しく実装する必要があります。これは、コードの冗長性を意味しますが、この場合はそれほど多くありません。

どのアプローチが一番好きかを自分で決める必要があります。個人的には、クラスをインターフェースに変換することをお勧めします。

ヒント:Typescriptは、ゲッターとセッターの構文糖衣構文であるプロパティを提供します。あなたはこれを見たいかもしれません:http//blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/


2
面白い。1)拡張PIXIし、ライブラリを変更してその上に別のクラスを追加できないという理由だけで、それを行うことはできません。2)それは私が使用できる可能な解決策の1つですが、可能であればそれを避けたいと思いました。3)私は間違いなくそのコードを複製したくありません、それは今は簡単かもしれませんが、次に何が起こるのでしょうか?私はこのプログラムに1日しか取り組んでいませんが、後でさらに多くのことを追加しますが、私にとっては良い解決策ではありません。詳細な回答をありがとう、ヒントを見てみましょう。
vadorequest 2014年

確かに興味深いリンクですが、ここで問題を解決するためのユースケースは見当たりません。
vadorequest 2014年

これらすべてのクラスがPIXIを拡張する場合は、ObjectThatShouldAlsoBeExtendedクラスにPIXIを拡張させ、そこからTexture、Sprite、...クラスを派生させます。それが、クラスをhirarchyタイプに挿入することで意味したことです
lhk

PIXIそれ自体はクラスではなく、モジュールであり、拡張することはできませんが、そうであれば、それは実行可能なオプションでした。
vadorequest 2014年

1
それで、各クラスはPIXIモジュールから別のクラスを拡張しますか?そうです、ObjectThatShouldAlsoクラスをhirarchy型に挿入することはできません。それは可能ではありません。それでも、has-a関係またはインターフェースを選択できます。すべてのクラスが共有する共通の動作を説明したいので、インターフェースを使用することをお勧めします。コードが重複している場合でも、これが最もクリーンな設計です。
lhk 2014年

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

非常にハッキーな解決策は、新しい親クラスに関数を1つずつ追加することで継承するクラスをループすることです。

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'



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

デザインパターンには、「継承よりも構成を優先する」という原則があります。クラスAからクラスBを継承する代わりに、クラスAのインスタンスをクラスB内にプロパティとして配置すると、クラスAの機能をクラスB内で使用できるようになりますここここでその例をいくつか見ることができます

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.