Mixin(ミックスイン)

《 初回公開:2022/11/17 , 最終更新:未 》

【 目次 】

ミックスインについては「不思議の国のJavaScript オブジェクト指向に関するtips - 愚鈍人のクラスの継承とミックスインの項」でだいぶ以前に考察してみたが、classキーワードも加わって新たな状況になったので。

多重継承は菱形継承問題という問題があってクラスの多重継承をサポートしていない言語が多い。

菱形継承問題というのは単純に言うと、多重継承(複数のクラスを継承)を許すと継承したスーパークラスに同名のメソッドが存在した場合、どのメソッドを呼び出したらいいのかわからなくなってしまう事のようだ。

C#やJava言語ではクラスの多重継承をサポートしていないかわりにインタフェースの多重継承はサポートされている。
JavaScriptもクラスの多重継承をサポートしていないが、ミックスインという手法を使って菱形継承問題を回避しつつ多重継承のようなものを実装する事ができる。

MixinまたはMix-in(ミックスイン)は[1][2][3][4]、オブジェクト指向プログラミングで用いられる技法であり、他のクラスから使用されるメソッド群を持つクラスが、他のクラスのスーパークラスにならないで済むための、特別な多重継承関係を実現するためのメカニズムを意味している。Mix-inされたメソッドに、他のクラスがアクセスする方法はそれぞれの言語仕様に依存している。

Mix-inはコードの再利用性を促進し、従来のクラスの多重継承がもたらしていた菱形継承などの数々の問題を回避する[5]。多重継承を採用していない言語においては、多重継承と類似の機能性をより堅牢にして提供する。

ミックスインはクラスの多重継承をおこなわずに複数のクラスのプロパティやメソッドの機能をクラスに組み入れようとする手法と言えるのかな。

ミックスインを実現するための手法はいろいろあって

単一継承でクラスを連結

MDNのページより参考にしたコードを以下に示す。

let calculatorMixin = Base => class extends Base {
  calc() {
    console.log("calc")
  }
};

let randomizerMixin = Base => class extends Base {
  randomize() {
    console.log("randomize")
  }
};
class Foo { }
class Bar extends calculatorMixin(randomizerMixin(Foo)) { }
let bar = new Bar();
bar.calc(); // calc
bar.randomize();    // randomize

このコードはわかりづらいので、もっと単純化したコードに解読してみると

class Foo { }
class clsA extends Foo {
  calc() {
    console.log("calc")
  }
}
class clsB extends clsA {
  randomize() {
    console.log("randomize")
  }
}
class Bar extends clsB { }

let bar = new Bar();
bar.calc(); // calc
bar.randomize();    // randomize
}

つまり、継承したい複数のクラスFooとclsA,clsBを多重継承するのではなく、単一継承でシリーズにそれぞれ連結してしまおうという理屈である。
継承関係はFoo⇒clsA⇒clsB⇒Barとなる。
これによりBarクラスのインスタンスはFooクラスを継承しつつclsAのcalcメソッドとclsBのrandomizeメソッドの両方を実行できるようになる。

Object.assignメソッドを使って他のクラスのプロパティをコピー

SuperClsを継承したSubClsにMixinClsクラスをミックスインしたいのだが

うまくいかない例。

class SuperCls { }
class SubCls extends SuperCls { }
// これはうまくいかない
class MixinCls {
  _seed = 3;
  // ゲッター
  get seed() {
    return this._seed;
  }

  // セッター
  set seed(seed) {
    this._seed = seed
  }

  // メソッド
  multiSeed(multi) {
    return this._seed * multi;
  }

  // ジェネレーターメソッド
  *g() {
    let index = 0;
    while (true) {
      yield index += this._seed;
    }
  }
}

let subCls = new SubCls();
// インスタンスのメンバーをコピー
Object.assign(subCls,new MixinCls());
// prototypeのメンバーは列挙可能ではないようでメソッドのコピーができない。
Object.assign(SubCls.prototype, MixinCls.prototype);

いろいろ試行錯誤して、たどり着いたコードは

// クラス定義
class SuperCls { }
class SubCls extends SuperCls {
  // ミックスインクラスのメンバー変数(データフィールド)はコンストラクタ関数とかクラス内で定義
  _seed = 3;
}
// prototypeに格納したいメソッド類はオブジェクト内に定義してassignメソッドで注入
let mixinMethod = {
  get seed() {
    return this._seed;
  },

  // セッター
  set seed(seed) {
    this._seed = seed
  },

  // メソッド
  multiSeed(multi) {
    return this._seed * multi;
  },

  // ジェネレーターメソッド
  *g() {
    let index = 0;
    while (true) {
      yield index += this._seed;
    }
  }
}
Object.assign(SubCls.prototype, mixinMethod);

動作確認

// ミックスインできてるか確認
let subCls = new SubCls();
// ゲッタ-, セッターの呼び出し
subCls.seed = 5;
console.log(subCls.seed); // 5

// メソッドの呼び出し
console.log(subCls.multiSeed(4)); // 20

// ジェネレーターメソッドの呼び出し
const it = subCls.g()
for (let i = 0; i < 5; i++) {
  console.log(it.next().value); // 5, 10, 15, 20, 25
}

// _seedはプライベート変数ではないので直接アクセス可能
subCls._seed = 99;
ページのトップへ戻る