TypeScript Handbook を読み進めていく第二十回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes
- Functions
- Generics
- Enums
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules
- Namwspaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators
- Mixins (今ココ)
- Triple-Slash Directives
- Type Checking JavaScript Files
Mixins
Introduction
コンポーネントを再利用してクラスを構築する方法として、伝統的なオブジェクト指向の他に、より単純なクラスを組み合わせる方法も一般的です。
Scala のような言語で mixin や trait と呼ばれるものについてご存知かもしれませんが、このパターンは JavaScript においてもよく用いられます。
Mixin sample
以下のコードは TypeScript における mixin の実現例です。
// 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 つのクラスが登場しました。
これらのクラスは固有の機能を持っており、後でそれらの機能を新しいクラスに適用しています。
// 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 を組み合わせた新しいクラスを作成しています。
class SmartObject implements Disposable, Activatable {
まず気付くのが、extends
ではなく、implements
を使用している点です。
これはクラスをインタフェースとして用いるという意味であり、Disposable と Activatable の型を、その実装を除いて使用することになります。
つまり、これらのクラスの実装を別に提供する必要があるということです。
それ以外の部分については mixin を用いて避けたいところです。
「実装の提供」以外というと、メンバの宣言のことかな?
この要件を満たすために Mixin のメンバと同じ型のプロパティを代理プロパティとして用意します。
これにより、実行時にこれらのメンバにアクセスすることが可能になりますが、帳簿上のオーバーヘッドが生じます。
要は Mixin にあわせてメンバを宣言しないといけないから面倒くさいよねという話
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
なんでインタフェースを継承してるのにまたメンバ宣言が必要なのか疑問だったけど、Java とかのインタフェースと違って、TypeScript のインタフェースは「そのメソッド/プロパティを継承クラスで宣言していることを強制する」ものだから、SmartObject クラスでまた宣言が必要というわけだ
最後に、mixin をクラスに混ぜあわせ、完全な実装を提供します。
applyMixins(SmartObject, [Disposable, Activatable]);
mixin を混ぜあわせるためのヘルパ関数では、各 mixin のプロパティを走査し、その実装とともに mixin の対象クラスにコピーします。
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
なんでこんな面倒なことをする必要があるのか不思議に思うかもしれないけど、TypeScript では多重継承を禁止しているため、複数のクラスを再利用したい場合はこういう(インタフェースとして継承して、実装をコピー)トリッキーなことをする必要があるというわけだ。
もちろん、代用プロパティの宣言とapplyMixins
の適用さえすればDisposable
やActivatable
を継承する必要はないけれども、メンバの型や引数が変更された時にコンパイルエラーにさせるために、あえてインタフェースとして継承しているのだろう。