web-dev-qa-db-ja.com

TypeScript:クラス構成

MPJによるこのすばらしい Composition over Inheritance ビデオに基づいて、TypeScriptでコンポジションを作成しようとしました。オブジェクトやファクトリー関数ではなく、classesを作成したい。これまでの私の取り組みは次のとおりです(lodashから少し助けて):

class Barker {
  constructor(private state) {}

  bark() {
    console.log(`Woof, I am ${this.state.name}`);
  }
}

class Driver {
  constructor(private state) {}

  drive() {
    this.state.position = this.state.position + this.state.speed;
  }
}

class Killer {
  constructor(private state) {}

  kill() {
    console.log(`Burn the ${this.state.prey}`);
  }
}

class MurderRobotDog {
  constructor(private state) {
    return _.assignIn(
      {},
      new Killer(state),
      new Driver(state),
      new Barker(state)
    );
  }
}

const metalhead = new MurderRobotDog({ 
  name: 'Metalhead', 
  position: 0, 
  speed: 100, 
  prey: 'witch' 
});

metalhead.bark(); // expected: "Woof, I am Metalhead"
metalhead.kill(); // expected: "Burn the witch"

これは次の結果になります:

TS2339:プロパティ 'bark'はタイプ 'MurderRobotDog'に存在しません

TS2339:プロパティ 'kill'はタイプ 'MurderRobotDog'に存在しません

TypeScriptでクラス構成を行う正しい方法は何ですか?

8
Glenn Mohammad

構成と継承

構成と継承を区別し、何を達成しようとしているのかを再考すべきだと思います。コメンターが指摘したように、MPJは実際にはミックスインを使用する例です。これは基本的に継承の形式であり、ターゲットオブジェクトに実装を追加します(混合)。

多重継承

私はこれを行うためのきちんとした方法を考え出そうとしました、そしてこれは私の最善の提案です:

type Constructor<I extends Base> = new (...args: any[]) => I;

class Base {}

function Flies<T extends Constructor<Base>>(constructor: T = Base as any) {
  return class extends constructor implements IFlies {
    public fly() {
      console.log("Hi, I fly!");
    }
  };
}

function Quacks<T extends Constructor<Base>>(constructor: T = Base as any) {
  return class extends constructor implements ICanQuack {
    public quack(this: IHasSound, loud: boolean) {
      console.log(loud ? this.sound.toUpperCase() : this.sound);
    }
  };
}

interface IHasSound {
  sound: string;
}

interface ICanQuack {
  quack(loud: boolean): void;
}

interface IQuacks extends IHasSound, ICanQuack {}

interface IFlies {
  fly(): void;
}

class MonsterDuck extends Quacks(Flies()) implements IQuacks, IFlies {
  public sound = "quackly!!!";
}

class RubberDuck extends Quacks() implements IQuacks {
  public sound = "quack";
}

const monsterDuck = new MonsterDuck();
monsterDuck.quack(true); // "QUACKLY!!!"
monsterDuck.fly(); // "Hi, I fly!"

const rubberDuck = new RubberDuck();
rubberDuck.quack(false); // "quack"

このアプローチを使用する利点は、継承されたメソッドの実装で所有者オブジェクトの特定のプロパティへのアクセスを許可できることです。もう少し良いネーミングを使用することもできますが、これは非常に潜在的な解決策だと思います。

組成

コンポジションは、関数をオブジェクトに混ぜるのではなく、その中にどのビヘイビアーを含めるかを設定し、それらをオブジェクト内の自己完結型ライブラリとして実装します。

interface IQuackBehaviour {
  quack(): void;
}

interface IFlyBehaviour {
  fly(): void;
}

class NormalQuack implements IQuackBehaviour {
  public quack() {
    console.log("quack");
  }
}

class MonsterQuack implements IQuackBehaviour {
  public quack() {
    console.log("QUACK!!!");
  }
}

class FlyWithWings implements IFlyBehaviour {
  public fly() {
    console.log("I am flying with wings");
  }
}

class CannotFly implements IFlyBehaviour {
  public fly() {
    console.log("Sorry! Cannot fly");
  }
}

interface IDuck {
  flyBehaviour: IFlyBehaviour;
  quackBehaviour: IQuackBehaviour;
}

class MonsterDuck implements IDuck {
  constructor(
    public flyBehaviour = new FlyWithWings(),
    public quackBehaviour = new MonsterQuack()
  ) {}
}

class RubberDuck implements IDuck {
  constructor(
    public flyBehaviour = new CannotFly(),
    public quackBehaviour = new NormalQuack()
  ) {}
}

const monsterDuck = new MonsterDuck();
monsterDuck.quackBehaviour.quack(); // "QUACK!!!"
monsterDuck.flyBehaviour.fly(); // "I am flying with wings"

const rubberDuck = new RubberDuck();
rubberDuck.quackBehaviour.quack(); // "quack"

ご覧のとおり、実用的な違いは、コンポジットはそれを使用するオブジェクトに存在するプロパティを認識しないことです。これは継承よりも構成の原則に準拠しているため、おそらく良いことです。

8
nomadoda

残念ながら、これを行う簡単な方法はありません。現在、これを可能にするextendsキーワードを許可する提案がありますが、それは このGitHubの問題 でまだ議論されています。

他の唯一のオプションは、TypeScriptで使用可能な Mixins機能 を使用することですが、そのアプローチの問題は、「継承された" クラス。

1
th3n3wguy