Edited at

[JavaScript] そんな継承はイヤだ - クラス定義 - オブジェクト作成

More than 1 year has passed since last update.

JavaScript のオブジェクト作成においてクラス定義で継承を実装する方法はいくつかあります。

正しい継承はどうあるべきか、基本から検証しながら考えてみたいと思います。

※正しくクラス定義がエコ楽にできる様に追加記事書きました。

[JavaScript] getter/setterも使えるエコ楽なクラス定義 - もちろん継承も - private変数も


一番簡単なオブジェクトの作成方法

典型的な JavaScript のオブジェクトを簡単に作成してみて、それらを確認してみましょう。

var obj1 = {x: 12, y: "ab"};

var obj2 = new Object; // または new Object()
obj2.x = 34;
obj2.y = "cd";
// obj < Object

var obj3 = [12, "ab"];
var obj4 = new Array(34, "cd");
// obj < Array < Object


instanceof による確認

instanceof を使用すると Object かどうかがすぐにわかります。

console.log(obj1 instanceof Object);  // -> true

console.log(obj2 instanceof Object); // -> true
console.log(obj3 instanceof Object); // -> true
console.log(obj4 instanceof Object); // -> true

true なので、全て Object の一種です。間違いありません。


constructor による確認

constructor を使用すると、オブジェクトが誰によって作成されたのか、つまりどのクラスのオブジェクトなのか確認することができます。

console.log(obj1.constructor === Object);  // -> true

console.log(obj2.constructor === Object); // -> true
console.log(obj3.constructor === Array); // -> true
console.log(obj4.constructor === Array); // -> true

後半の例は配列 Array です。思った通りですね。


もう一度 instanceof による確認

それでは Array なのかどうか instanceof で確認してみましょう。

console.log(obj1 instanceof Array);  // -> false

console.log(obj2 instanceof Array); // -> false
console.log(obj3 instanceof Array); // -> true
console.log(obj4 instanceof Array); // -> true

後半は配列なので true ですが、前半は配列ではありませんので false です。予想通りで良かったですね。


更に __proto__prototype による確認

クラスの prototype はどうなっているのか。

それは作成した各オブジェクトの __proto__ 属性からポイントされています。

console.log(obj1.__proto__ === Object.prototype);  // -> true

console.log(obj2.__proto__ === Object.prototype); // -> true
console.log(obj3.__proto__ === Array.prototype); // -> true
console.log(obj4.__proto__ === Array.prototype); // -> true

// getProto(obj)
var getProto = Object.getPrototypeOf ? Object.getPrototypeOf :
function getProto(obj) { return obj.__proto__; };

console.log(getProto(obj1) === Object.prototype); // -> true
console.log(getProto(obj2) === Object.prototype); // -> true
console.log(getProto(obj3) === Array.prototype); // -> true
console.log(getProto(obj4) === Array.prototype); // -> true

それぞれのクラスのプロトタイプ・オブジェクトをポイントしているので問題ないですね。


__proto____proto__ による確認

ついでに __proto____proto__ も確認しておきましょう。

console.log(obj1.__proto__.__proto__ === null);              // -> true

console.log(obj2.__proto__.__proto__ === null); // -> true
console.log(obj3.__proto__.__proto__ === Object.prototype); // -> true
console.log(obj4.__proto__.__proto__ === Object.prototype); // -> true
console.log(obj3.__proto__.__proto__.__proto__ === null); // -> true
console.log(obj4.__proto__.__proto__.__proto__ === null); // -> true

console.log(getProto(getProto(obj1)) === null); // -> true
console.log(getProto(getProto(obj2)) === null); // -> true
console.log(getProto(getProto(obj3)) === Object.prototype); // -> true
console.log(getProto(getProto(obj4)) === Object.prototype); // -> true
console.log(getProto(getProto(getProto(obj3))) === null); // -> true
console.log(getProto(getProto(getProto(obj4))) === null); // -> true

Object の親クラスは null に行き着いておしまい。

Array の親クラスは Object で、更に親はいない、という事ですね。

この先祖をたぐれる様になっている仕組みがプロトタイプ・チェーンです。

ちなみに __proto__ は非標準です。

標準では Object.getPrototypeOf を使う様に、との事。


今度は constructor.name による確認

constructor はオブジェクトを作成する時に使用されたコンストラクタ関数を指しており、関数に名前がある場合 name 属性を見れば、その関数名がわかります。

console.log(obj1.constructor.name);  // -> Object

console.log(obj2.constructor.name); // -> Object
console.log(obj3.constructor.name); // -> Array
console.log(obj4.constructor.name); // -> Array

正しく表示できましたか。もし思った通りに表示されない場合、以下の互換性対応のおまじないコードを先に実行して、もう一度上記のコードを実行してくれますか。うまくいく事を祈ります。

// 互換性対応のおまじない。主に IE。

var fnameRegExp = /^\s*function\s*\**\s*([^\(\s]*)[\S\s]+$/im;

// fname: get function name
function fname() {
return ('' + this).replace(fnameRegExp, '$1');
}

// Function.prototype.name
if (!Function.prototype.hasOwnProperty('name')) {
if (Object.defineProperty)
Object.defineProperty(Function.prototype, 'name', {get: fname});
else if (Object.prototype.__defineGetter__)
Function.prototype.__defineGetter__('name', fname);
}

関数の name 属性は非標準なので本来は使用してはいけないのですが、デバッグする時に非常に役に立つので上記のコードを実行してから使う事にします。あしからず。


データだけでなくメソッドも定義してみる

幅(w)と高さ(h)で四角形の面積を計算してみる。

var x1 = {w: 10, h: 20, calc: function () { return this.w * this.h; }};

var x2 = {w: 20, h: 30, calc: function () { return this.w * this.h; }};

console.log(x1.calc()); // -> 200
console.log(x2.calc()); // -> 600
console.log(x1.calc === x2.calc); // -> false
console.log(x1.calc.toString() === x2.calc.toString()); // -> true

メソッドとしての関数を記憶するためのフィールドが、オブジェクトのインスタンスそれぞれに必要となりますね。同じ様に関数定義しても違う関数が登録されている様です。またメソッドの数だけ記憶領域が必要です。あまり効率が良いとは言えませんね。

ちょっとだけマシな定義にするには以下の様にすればいいのでしょうか。

function calc() {

return this.w * this.h;
}

var x3 = {w: 10, h: 20, calc: calc};
var x4 = {w: 20, h: 30, calc: calc};

console.log(x3.calc()); // -> 200
console.log(x4.calc()); // -> 600
console.log(x3.calc === x4.calc); // -> true

面積をプロパティの様に扱う事も可能です。

その様な場合 getter を使います。

var x5 = {w: 10, h: 20, get area() { return this.w * this.h; }};

var x6 = {w: 20, h: 30, get area() { return this.w * this.h; }};

console.log(x5.area); // -> 200
console.log(x6.area); // -> 600

ここには例はあげませんでしたが setter も勉強しておきましょう。

こちらに示したサンプルは非常に簡単ですが、本格的にオブジェクトをたくさん作成するには、記憶領域の効率はあまり良くないと思います。

直接オブジェクトを作成するパターンはこれくらいにして本題に進みましょう。


プロトタイプベースのオブジェクト指向

みなさんはクラスベースのオブジェクト指向言語として Java, C#, C++ なども勉強されているのではないかと思いますが、JavaScript はプロトタイプベースのオブジェクト指向言語なので、ちょっと変わった振る舞いをします。様々なクラス定義の例をみながら検証していきましょう。


シンプルなクラス定義

JavaScript ではコンストラクタ関数を定義する事で、クラス定義の様に、記述する事が可能です。

まずは Animal クラスを定義してみましょう。

// Animal クラス定義

function Animal(name) {
this.name = name;
}

// Animal クラスのメソッド定義
Animal.prototype.introduce = function introduce() {
console.log('私は ' + this.constructor.name + '' + this.name + ' です。');
};


インスタンス作成と利用

では Animal クラスのインスタンスオブジェクトを作成し、利用してみましょう。

// Animal クラスのインスタンスオブジェクトの作成と利用

var a1 = new Animal('Annie');
a1.introduce(); // -> 私は Animal の Annie です。

this.constructor.name が使用できるように、上記の様に関数定義としてクラス名を関数に付ける方が良いと思います。


インスタンスオブジェクトの検証

では、このクラスのインスタンスがちゃんとできているか検証してみましょう。

まずはクラス定義とオブジェクトが正しく作成できているか検証するための関数を作りましょう。

var CSI    = '\u001b[';  // ANSI Control Sequence Introducer

var NORMAL = typeof window !== 'undefined' ? '' : CSI + 'm';
var GREEN = typeof window !== 'undefined' ? '' : CSI + '32m';
var RED = typeof window !== 'undefined' ? '' : CSI + '31m';
var YELLOW = typeof window !== 'undefined' ? '' : CSI + '33m';

// getProto(obj)
var getProto = Object.getPrototypeOf ? Object.getPrototypeOf :
function getProto(obj) { return obj.__proto__; };

// assertTrue: true であればOK、false の時はエラーメッセージを表示する
function assertTrue(bool, msg) {
if (!bool) {
console.error(RED + 'Error: ' + msg + NORMAL);
}
}

// verifyClassObject: オブジェクトの検証
function verifyClassObject(obj, expected, keysExpected) {
var name = expected[0];
var TheClass = expected[1];
var SuperClass = expected[2];

var keys = [];
for (var i in obj) {
keys.push(i);
}
var keysActual = keys.join(',');
if (keysActual === keysExpected) {
console.info(GREEN + 'Success: keys = ' + keysActual + NORMAL);
}
else {
console.error(RED + 'Error: keys = ' + keysActual + ', ' + NORMAL +
YELLOW + 'Expected: keys ' + keysExpected + NORMAL);
}

// obj は Class のインスタンスだ (new Class で作成したからね)
assertTrue(obj instanceof TheClass,
name + '' + TheClass.name + ' のインスタンスではない。');

// obj は SuperClass のインスタンスでもある
if (SuperClass) {
assertTrue(obj instanceof SuperClass,
name + '' + SuperClass.name + ' のインスタンスではない。');
}

// obj は Object のインスタンスでもある
assertTrue(obj instanceof Object,
name + '' + Object.name + ' のインスタンスではない。');

// obj のコンストラクタは Class だ
assertTrue(obj.constructor === TheClass,
name + ' のコンストラクタは ' + obj.constructor.name + ' で、 ' +
TheClass.name + ' ではない。');

// Class のプロトタイプオブジェクトのコンストラクタは Class だ
assertTrue(TheClass.prototype.constructor === TheClass,
TheClass.name + ' のプロトタイプは ' + TheClass.prototype.constructor.name + ' で、 ' +
TheClass.name + ' ではない。');

// obj の __proto__ を見てみると...
assertTrue(getProto(obj).constructor === TheClass,
name + ' の __proto__ は ' + getProto(obj).constructor.name + ' で、 ' +
TheClass.name + ' ではない。');

// Class は SuperClass を継承しているんだね
if (SuperClass) {
assertTrue(getProto(getProto(obj)) === SuperClass.prototype &&
getProto(getProto(obj)).constructor === SuperClass &&
getProto(TheClass.prototype).constructor === SuperClass,
name + ' の __proto__ の __proto__ は ' +
getProto(getProto(obj)).constructor.name + ' で、 ' +
SuperClass.name + ' ではない。');
}

// obj の先祖を辿ってみる...
var expectedString = expected.map(function (fn) {
return typeof fn === 'function' ? fn.name : fn;
}).join(' < ');

var ancestors = [name];
for (var obj = getProto(obj); obj; obj = getProto(obj)) {
ancestors.push(obj.constructor.name);
}

var actualString = ancestors.join(' < ');
if (actualString === expectedString) {
console.info(GREEN + 'Success: ' + actualString + NORMAL);
}
else {
console.error(RED + 'Error: ' + actualString + ', ' + NORMAL +
YELLOW + 'Expected: ' + expectedString + NORMAL);
}
// -> name < Class < SuperClass < Object
}

if (!('info' in console)) { console.info = console.log; }
if (!('error' in console)) { console.error = console.log; }


a1 < Animal < Object を検証する

では作成したオブジェクトを検証してみましょう。

// a1 < Animal < Object かどうか検証してみる

verifyClassObject(a1, ['a1', Animal, Object], 'name,introduce');
// -> Success: keys = name,introduce
// -> Success: a1 < Animal < Object

もちろん、シンプルな例ですので、特に問題はありませんね。


関数定義には名前を付ける

a1.constructor.name で検証ができる様に関数定義には名前を付ける方が良いと考えています。エラー発生時のトレースバック情報にも関数名が含まれますので、そういう場合にも詳しい情報が表示されるので有効です。それはメソッドの関数名も同様です。

もちろん、以下の様な形式でクラス定義してもいいですが、名前が無いクラスを作っているようなものなので、私は推奨はしません。

var Animal = function (name) {

this.name = name;
};

せめて以下の様に名前を付けましょう。

var Animal = function Animal(name) {

this.name = name;
};


さきほどの簡単なオブジェクトもついでに検証

さきほど作成した簡単なオブジェクト obj1obj2, x1x6 も検証してみましょう。

verifyClassObject(obj1, ['obj1', Object], 'x,y');

// -> Success: keys = x,y
// -> Success: obj1 < Object

verifyClassObject(obj2, ['obj2', Object], 'x,y');
// -> Success: keys = x,y
// -> Success: obj2 < Object

verifyClassObject(obj3, ['obj3', Array, Object], '0,1');
// -> Success: keys = 0,1
// -> Success: obj3 < Array < Object

verifyClassObject(obj4, ['obj4', Array, Object], '0,1');
// -> Success: keys = 0,1
// -> Success: obj4 < Array < Object

verifyClassObject(x1, ['x1', Object], 'w,h,calc');
// -> Success: keys = w,h,calc
// -> Success: x1 < Object

verifyClassObject(x2, ['x2', Object], 'w,h,calc');
// -> Success: keys = w,h,calc
// -> Success: x2 < Object

verifyClassObject(x3, ['x3', Object], 'w,h,calc');
// -> Success: keys = w,h,calc
// -> Success: x3 < Object

verifyClassObject(x4, ['x4', Object], 'w,h,calc');
// -> Success: keys = w,h,calc
// -> Success: x4 < Object

verifyClassObject(x5, ['x5', Object], 'w,h,area');
// -> Success: keys = w,h,area
// -> Success: x5 < Object

verifyClassObject(x6, ['x6', Object], 'w,h,area');
// -> Success: keys = w,h,area
// -> Success: x6 < Object

当然ですが、全て問題ありません。そうなる様に引数をセットしました。


やっちゃいけない継承、その1

それでは Animal クラスを継承して Bear クラスを定義してみましょう。

// Bear クラス定義

function Bear(name) {
Animal.call(this, name);
}

// やっちゃいけない継承、その1
Bear.prototype = Animal.prototype;

// Bear クラスのインスタンスオブジェクトの作成と利用
var b1 = new Bear('Pooh');
b1.introduce(); // -> 私は Animal の Pooh です。

// b1 < Bear < Animal < Object かどうか検証してみる
verifyClassObject(b1, ['b1', Bear, Animal, Object], 'name,introduce');
// -> Success: keys = name,introduce
// -> Error: b1 のコンストラクタは Animal で、 Bear ではない。
// -> Error: Bear のプロトタイプは Animal で、 Bear ではない。
// -> Error: b1 の __proto__ は Animal で、 Bear ではない。
// -> Error: b1 の __proto__ の __proto__ は Object で、 Animal ではない。
// -> Error: b1 < Animal < Object, Expected: b1 < Bear < Animal < Object

なんとなく動いているように見えるんですけど、検証結果はどうでしょうか。

いくつか問題がありそうです。

b1 < Bear < Animal < Object っていうのがいいと思うけど、なんか違うね。

では、以下の様な Bear クラスのメソッド定義をするとどうなるでしょうか。

Bear.prototype.bearMethod = function bearMethod() {};

そうです。Animal クラスにも同じ定義ができてしまいますね。

これでは、おかしなことになりそうですね。

もし上記を実行してしまったなら以下で削除しておきましょう。

delete Bear.prototype.bearMethod;


やっちゃいけない継承、その2

今度は Animal クラスを継承して Cat クラスを定義してみましょう。

// Cat クラス定義

function Cat(name) {
Animal.call(this, name);
}

// やっちゃいけない継承、その2
Cat.prototype = new Animal;

// Cat クラスのインスタンスオブジェクトの作成と利用
var c1 = new Cat('Kitty');
c1.introduce(); // -> 私は Animal の Kitty です。

// c1 < Cat < Animal < Object かどうか検証してみる
verifyClassObject(c1, ['c1', Cat, Animal, Object], 'name,introduce');
// -> Success: keys = name,introduce
// -> Error: c1 のコンストラクタは Animal で、 Cat ではない。
// -> Error: Cat のプロトタイプは Animal で、 Cat ではない。
// -> Error: c1 の __proto__ は Animal で、 Cat ではない。
// -> Error: c1 < Animal < Animal < Object, Expected: c1 < Cat < Animal < Object

なんとなく動いているように見えるけど、検証結果はどうでしょうか。

やはり、いくつか問題がありそうです。

c1 < Cat < Animal < Object っていうのがいいと思うけど、やっぱりなんか違うね。


結果は正しそうだけど互換性が無い継承

今度は constructor__proto__ を使って Animal クラスを継承して Dog クラスを定義してみましょう。

// Dog クラス定義

function Dog(name) {
this.name = name;
}

// 無理やり constructor と __proto__ を使って prototype オブジェクトを上書きする
Dog.prototype = {
constructor: Dog,
__proto__: Animal.prototype
};

// Dog クラスのインスタンスオブジェクトの作成と利用
var d1 = new Dog('Hachi');
d1.introduce(); // -> 私は Dog の Hachi です。

// d1 < Dog < Animal < Object かどうか検証してみる
verifyClassObject(d1, ['d1', Dog, Animal, Object], 'name,introduce');
// -> Error: keys = name,constructor,introduce, Expected: keys name,introduce
// -> Success: d1 < Dog < Animal < Object

検証結果は正しそうでしたが for in で余計な constructor が出てきました。

また __proto__ を使うのは非標準です。


ところで ClassName.prototype = {} って、やっていいの?

なんか、これが問題になっているような気がしますね。

こういう ClassName.prototype = {} という記述を見たら間違いと思った方がいいでしょうね。


結果は正しいけど互換性が無い継承

今度は __proto__ だけを使用して Animal クラスを継承して Elephant クラスを定義してみましょう。

// Elephant クラス定義

function Elephant(name) {
Animal.call(this, name);
}

// 結果は正しいけど互換性が無い継承
Elephant.prototype.__proto__ = Animal.prototype;

// Elephant クラスのインスタンスオブジェクトの作成と利用
var e1 = new Elephant('Dumbo');
e1.introduce(); // -> 私は Elephant の Dumbo です。

// e1 < Elephant < Animal < Object かどうか検証してみる
verifyClassObject(e1, ['e1', Elephant, Animal, Object], 'name,introduce');
// -> Success: keys = name,introduce
// -> Success: e1 < Elephant < Animal < Object

検証結果は正しそうですね。

ですが __proto__ は標準ではありません。

互換性のないキーワードを使っている事が問題です。


正しそうな継承

今度は Object.createconstructor を使って Animal クラスを継承して Fox クラスを定義してみましょう。

// Fox クラス定義

function Fox(name) {
Animal.call(this, name);
}

Fox.prototype = Object.create(Animal.prototype);
Fox.prototype.constructor = Fox;

// Fox クラスのインスタンスオブジェクトの作成と利用
var f1 = new Fox('Gon');
f1.introduce(); // -> 私は Fox の Gon です。

// f1 < Fox < Animal < Object かどうか検証してみる
verifyClassObject(f1, ['f1', Fox, Animal, Object], 'name,introduce');
// -> Error: keys = name,constructor,introduce, Expected: keys name,introduce
// -> Success: f1 < Fox < Animal < Object

検証結果は正しそうでしたが for in で余計な constructor が出てきました。

やはり enumerable: false でないとまずいですね。


正しい継承

今度は Animal クラスを継承して Gorilla クラスを定義してみましょう。

継承させるための関数 inherits として Node.js の util.inherits をそのまま使用してみましょう。

// Gorilla クラス定義

function Gorilla(name) {
Animal.call(this, name);
}

// console.log(require('util').inherits.toString()); より
function inherits(ctor, superCtor) {
if (ctor === undefined || ctor === null)
throw new TypeError('The constructor to `inherits` must not be ' +
'null or undefined.');

if (superCtor === undefined || superCtor === null)
throw new TypeError('The super constructor to `inherits` must not ' +
'be null or undefined.');

if (superCtor.prototype === undefined)
throw new TypeError('The super constructor to `inherits` must ' +
'have a prototype.');

ctor.super_ = superCtor;
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
// ctor.prototype = Object.create(superCtor.prototype, {
// constructor: {
// value: ctor,
// enumerable: false,
// writable: true,
// configurable: true
// }
// });
}

// 正しい継承
inherits(Gorilla, Animal);

// Gorilla クラスのインスタンスオブジェクトの作成と利用
var g1 = new Gorilla('Kong');
g1.introduce();
// -> 私は Gorilla の Kong です。

// g1 < Gorilla < Animal < Object かどうか検証してみる
verifyClassObject(g1, ['g1', Gorilla, Animal, Object], 'name,introduce');
// -> Success: keys = name,introduce
// -> Success: g1 < Gorilla < Animal < Object

検証結果も、もちろん定義も問題ありませんね。


おまけ1: new を付け忘れた時の対応

以下の様に new を付け忘れた場合どうなるでしょうか。

try {

var a2 = Animal('Annie');
} catch (err) {
console.log(RED + err + NORMAL);
}

this オブジェクトがグローバルオブジェクトになってしまいますので、以下の様にガードする様にしましょう。

// Animal2 クラス定義

function Animal2(name) {
if (!(this instanceof Animal2)) {
return new Animal2(name);
}
this.name = name;
}

var a2 = Animal2('Annie');


おまけ2: Closure を使って定義する

以下のコードを見てください。

var a3 = new Animal('Annie');

a3.introduce(); // -> 私は Animal の Annie です。
console.log(a3.name); // -> Annie
a3.name = 'Aho';
a3.introduce(); // -> 私は Animal の Aho です。
console.log(a3.name); // -> Aho

name 属性が外から書き換えられてしまいますね。

Java, C#, C++ 等の言語でいう所の private なフィールドは無いのでしょうか。

Closure を使うとそれに似た事ができます。

// Animal3 クラス定義

function Animal3(name) {
this.introduce = function introduce() {
console.log('私は ' + this.constructor.name + '' + name + ' です。');
};
}

var a3 = new Animal3('Annie');
a3.introduce(); // -> 私は Animal3 の Annie です。
console.log(a3.name); // -> undefined
a3.name = 'Aho';
a3.introduce(); // -> 私は Animal3 の Annie です。
console.log(a3.name); // -> Aho

それぞれのオブジェクトにメソッド数だけ関数定義が必要になりますが、外部から内部の変数へのアクセスは絶対に不可能になります。公開されたメソッドでないと内部の変数にアクセスすることができないので非常に安全です。

それぞれのオブジェクトに属性を定義していくのか、コンストラクタ関数内の変数を定義し、公開メソッドを定義していくのか、全く違った設計になると思います。

記憶領域の効率は良くないですし、若干遅いのであまりお勧めはしません。


おまけ3: クラス共通の属性定義

以下の様に prototype に共通の属性を定義すると、デフォルト値の様なものが定義できます。それを上書きすることもできます。

// Animal4 クラス定義

function Animal4(name) {
this.name = name;
this.animalProp = 123;
}

Animal4.prototype.animalCommonProp = 'abc';

var a4 = new Animal4('Annie');
console.log(a4.animalProp + ' ' + a4.animalCommonProp);
// -> 123 abc
a4.animalProp = 456;
a4.animalCommonProp = 'xyz';
console.log(a4.animalProp + ' ' + a4.animalCommonProp);
// -> 456 xyz
delete a4.animalProp;
delete a4.animalCommonProp;
console.log(a4.animalProp + ' ' + a4.animalCommonProp);
// -> undefined abc

最後の行、注意してくださいね。


参考文献


Node.js のリリースノートに正しい継承方法が記述されていました

v0.8 から v0.10 の主なAPI変更 - Node.js

EventEmitter を継承させる時の注意点として記述があります。


型判定

JavaScriptの「型」の判定について - Qiita

instanceof を正しく使える様に定義していくべきでしょうね。

型判定には typeof が高速なので、まずはそれを使うべきかなぁと思います。


重要な事が記述されています

Javascriptでオブジェクト指向するときに覚えておくべきこと - Qiita


以下のような記事を読んで奮起しました。

【メモ】JavaScriptで継承(っぽいの)- Qiita

っぽいんですけど、やはり...

javascriptでの継承の基本パターン4つ - Qiita

検証してみていない...