🫠
Mixinとインターフェースの違い
備忘録として
Mixinとインターフェースの書き方
JavaScriptにはPrototypeに動的にメソッドを追加できる。これはほかの言語では一般的にMixinと呼ばれる。
// Mixin用のオブジェクト
const Flyable = {
fly() {
console.log(`${this.name} is flying!`);
}
};
// コンストラクタ関数
function Bird(name) {
this.name = name;
}
// Mixin を prototype に適用
Object.assign(Bird.prototype, Flyable);
const eagle = new Bird("Eagle");
eagle.fly(); // "Eagle is flying!"
しかし、TypeScriptではこのような書き方はしない。Mixinよりもインターフェースによる明示的な実装が好まれる。
// Mixin用のインターフェース
interface Flyable {
fly(): void;
}
// メインクラス
class Bird implements Flyable {
name: string;
constructor(name: string) {
this.name = name;
}
fly(){
console.log(`${(this as any).name} is flying!`);
}
}
それぞれのメリデメ
Mixinのメリット
- 複数のクラスに共通の機能を追加しやすい
- 動的な機能追加ができる
- JavaScript(TypeScript)言語では動的に機能追加可能
- 既存のクラスに後から機能を追加できる
Mixinのデメリット
- 型安全性が低い
-
Object.assing()
でmixinすると型の整合性が保証されない。as any
を多用することに。
-
- 名前衝突のリスク
- Mixinによって同じメソッド名やプロパティ名が異なるクラスに追加されるため、名前衝突が起きやすくなる
インターフェースのメリット
- 型安全性が高い
- 設計が明確
- 明示的に機能を実装しないといけないため、設計からの落とし込みがしやすい
- 抽象化をサポート
- インターフェースによって、異なるクラス間で共通のメソッドを抽象化し、異なる実装を持つことができる。
インターフェースのデメリット
- コードの再利用性が低い
- インターフェースは定義を行うだけで、中身の実装までは共有しない
- 実装の強制
- 動的な機能追加ができない
- 実行時に動的に機能を追加することができない
考え方の違い
- 動的(状況に応じて役割を与える) vs 静的(最初から役者が揃っている)
- 使い方を複数の親に提供 vs ルールだけ決める
Mixin
- Mixinは同じ機能実装をほかのオブジェクトと共有しようという考え方。
- もう少し深ぼればエンティティロールを実現する手段として体系化できるかもしれない。使われる側が使われ方を親エンティティに提供する感じ。
- 既存のオブジェクトに動的に機能を追加できるかは言語による。多くの言語はクラス宣言時のみMixin可能で、インターフェースと併用する。
インターフェース
- インターフェースはメソッドの実装を強制して抽象化しようという考え方
- サブタイピングポリモーフィズムを実現する手段として体系化されている
- インターフェースは基本的に静的にオブジェクトを組み合わせていく考え方のため、動的な機能追加を好まない。最初からすべての役者が揃っている。
TypeScriptで型安全にMixinする方法
TypeScriptで型安全にMixinするには、既存のクラスを拡張するクラスを新しく作るのが無難。
どちらにしても、結局はインターフェースの実装によってしまう。
interfaceでクラスを拡張するパターン
// Mixin用のインターフェース
interface Flyable {
fly(): void;
}
// Mixin の実装(クラス)
class FlyableMixin {
fly() {
console.log(`${(this as any).name} is flying!`);
}
}
// クラスのコンストラクタを拡張するヘルパー関数
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
// メインクラス
class Bird {
name: string;
constructor(name: string) {
this.name = name;
}
}
// Mixin を適用
interface Bird extends Flyable {} // インターフェースのマージ。これがないとTypeErrorになる
applyMixins(Bird, [FlyableMixin]);
const eagle = new Bird("Eagle");
eagle.fly(); // "Eagle is flying!"
既存のクラスを拡張する
以下では既存のクラスを継承する形で新しくメソッドを定義している。
// Mixin の定義
interface Flyable {
fly(): void;
}
class FlyableMixin {
fly() {
console.log(`${(this as any).name} is flying!`);
}
}
// デコレータ関数
function mixinFlyable<T extends new (...args: any[]) => {}>(Base: T) {
return class extends Base {
fly() {
console.log(`${(this as any).name} is flying!`);
}
};
}
// ベースクラス
class Bird {
name: string;
constructor(name: string) {
this.name = name;
}
}
// デコレータで機能追加
const BirdWithFly = mixinFlyable(Bird);
const eagle = new BirdWithFly("Eagle");
eagle.fly(); // "Eagle is flying!"
明示的に継承を使うパターン。
// Mixin 用のインターフェース
interface Flyable {
fly(): void;
}
class FlyableMixin implements Flyable {
fly() {
console.log(`${(this as any).name} is flying!`);
}
}
// ベースクラス
class Bird {
name: string;
constructor(name: string) {
this.name = name;
}
}
// Bird クラスを拡張して Flyable を追加
class BirdWithFly extends Bird {
flyable: FlyableMixin;
constructor(name: string) {
super(name);
this.flyable = new FlyableMixin();
}
fly() {
this.flyable.fly();
}
}
const eagle = new BirdWithFly("Eagle");
eagle.fly(); // "Eagle is flying!"
どちらがよいのか
インターフェースにはなくMixinにはある唯一の強みは定義済みのオブジェクトに対して新しくクラスを定義せずに動的に機能を追加できることだと思う。しかし、その分型安全性を捨てている。型安全に動的に機能を追加しようとすれば、ベースのクラスと新しい機能を合体させた新しいクラスを定義する必要がある。
現状、僕の意見としては型安全性を捨ててまで、すでにあるオブジェクトに機能を追加する必要はないというもの。Mixinの考え方の背景にある「状況に応じて役割を変える」というものを実現させたい場合、もちろん既存のオブジェクトに動的に機能を追加できるのが理想かもしれないが、その場に応じて新しい機能を持った新しいクラスを定義するのでも事足りるはず。
結論
Mixinの背景の考え方を理解しつつ、インターフェースを使う
Discussion