12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScript Handbook を読む (20. Mixins)

Last updated at Posted at 2017-11-03

TypeScript Handbook を読み進めていく第二十回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namwspaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins (今ココ)
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Mixins

原文

Introduction

コンポーネントを再利用してクラスを構築する方法として、伝統的なオブジェクト指向の他に、より単純なクラスを組み合わせる方法も一般的です。
Scala のような言語で mixin や trait と呼ばれるものについてご存知かもしれませんが、このパターンは JavaScript においてもよく用いられます。

Mixin sample

以下のコードは TypeScript における mixin の実現例です。

TypeScript
// Disposable Mixin
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }

}

// Activatable Mixin
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

class SmartObject implements Disposable, Activatable {
    constructor() {
        setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
    }

    interact() {
        this.activate();
    }

    // Disposable
    isDisposed: boolean = false;
    dispose: () => void;
    // Activatable
    isActive: boolean = false;
    activate: () => void;
    deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);

let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);

////////////////////////////////////////
// どこかにある実行ライブラリ
////////////////////////////////////////

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

Understanding the sample

前述の例では最初に mixin として振る舞う 2 つのクラスが登場しました。
これらのクラスは固有の機能を持っており、後でそれらの機能を新しいクラスに適用しています。

TypeScript
// Disposable Mixin
class Disposable {
    isDisposed: boolean;
    dispose() {
        this.isDisposed = true;
    }

}

// Activatable Mixin
class Activatable {
    isActive: boolean;
    activate() {
        this.isActive = true;
    }
    deactivate() {
        this.isActive = false;
    }
}

続いてこの 2 つの mixin を組み合わせた新しいクラスを作成しています。

TypeScript
class SmartObject implements Disposable, Activatable {

まず気付くのが、extends ではなく、implements を使用している点です。
これはクラスをインタフェースとして用いるという意味であり、Disposable と Activatable の型を、その実装を除いて使用することになります。
つまり、これらのクラスの実装を別に提供する必要があるということです。
それ以外の部分については mixin を用いて避けたいところです。

「実装の提供」以外というと、メンバの宣言のことかな?

この要件を満たすために Mixin のメンバと同じ型のプロパティを代理プロパティとして用意します。
これにより、実行時にこれらのメンバにアクセスすることが可能になりますが、帳簿上のオーバーヘッドが生じます。

要は Mixin にあわせてメンバを宣言しないといけないから面倒くさいよねという話

TypeScript
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;

なんでインタフェースを継承してるのにまたメンバ宣言が必要なのか疑問だったけど、Java とかのインタフェースと違って、TypeScript のインタフェースは「そのメソッド/プロパティを継承クラスで宣言していることを強制する」ものだから、SmartObject クラスでまた宣言が必要というわけだ

最後に、mixin をクラスに混ぜあわせ、完全な実装を提供します。

TypeScript
applyMixins(SmartObject, [Disposable, Activatable]);

mixin を混ぜあわせるためのヘルパ関数では、各 mixin のプロパティを走査し、その実装とともに mixin の対象クラスにコピーします。

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

なんでこんな面倒なことをする必要があるのか不思議に思うかもしれないけど、TypeScript では多重継承を禁止しているため、複数のクラスを再利用したい場合はこういう(インタフェースとして継承して、実装をコピー)トリッキーなことをする必要があるというわけだ。
もちろん、代用プロパティの宣言と applyMixins の適用さえすれば DisposableActivatable を継承する必要はないけれども、メンバの型や引数が変更された時にコンパイルエラーにさせるために、あえてインタフェースとして継承しているのだろう。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?