ガチガチに固めた、静的な言語のようなクラスをJavaScript(TypeScript+ES6+ES7)で作ってみるシリーズ。
継承不可なクラス
「継承しないといけないクラス」は抽象クラスとしてTypeScript1.6から使えるようになった。が、逆の「継承してはいけないクラス」を定義することができない。
継承されるとまずいクラスがある場合、運用でカバーするしかないが、人間やるなと言われるとやりたくなるもので、運用でなんとかする方法論はだいたいうまくいかない。
/**
* いいか?おまえら。絶対継承するなよ?継承するなよ?
*/
class DontInherit{}
/**
* 「絶対○○するな」=「やれ」ということだな!
*/
class Oyakusoku extends DontInherit{}
/**
* やるなっていっただろー!?
*/
var ostrich = new Oyakusoku();
console.log(ostrich instanceof DontInherit); //true
@final
decorator
Javaのfinal classやC#のsealed classはそのような継承不可なクラスを定義するものだ。そこで@final
というデコレータをつくってみた。このデコレータを使うとクラスを継承しようとするとランタイムエラーが発生する。
@final class DontInherit{}
class Oyakusoku extends DontInherit{}
var ostrich = new Oyakusoku(); //runtime error! : A class "Oyakusoku" cannot inherit from @final class "DontInherit".
仕組み
仕組みは簡単で、コンストラクタ内の処理で基底クラス(のコンストラクタ関数)名とthis
オブジェクトのクラス(のコンストラクタ関数)名を比較して違っていたらエラーにするだけ。
const base = target; //targetは基底クラスのコンストラクタ
//中略
//thisのprototype経由でコンストラクタ名を取得
const thisname = Object.getPrototypeOf(this).constructor.name;
//名前が違っていたらエラーをthrow
if(thisname!==base.name){
throw Error(`A class "${thisname}" cannot inherit from @final class "${base.name}".`);
}
ES6(ES2015)から導入されたクラスだが、直接クラス名を取得する方法は無い.name
プロパティで仕様では取得できるが、多くの環境で非対応。ただし、ES6のクラスはシンタックスシュガーでしかないので、コンストラクタの関数名prototype.constructor.name
を取得することでクラス名を得る事ができる。
制限
名前で比較するので基底クラスに名前が無いと機能しない。もっとも、匿名クラスにデコレータはつけられない様子。
@final //error!: Decorators are not valid here
var AnonymousClass = class {}
なお、残念ながらbabelでは今回のコードは動かない。
コード
githubに今回作った@final
デコレータを公開している。