どうも。つじけ(tsujikenzo)です。このシリーズでは「Google Apps Scriptとドメイン駆動設計」についてお届けしています。
第1章は、「オブジェクト指向とはなにか」 を全3回でお送りします。
前回のおさらい
前回は、「オブジェクトとはなにか」で、オブジェクトとクラスの復習をお届けしました。
今回は、「クラスと継承」 をお届けします。
今日のアジェンダ
- JavaScriptの継承
- 継承の3タイプ
- 継承のルール
プログラム上で扱う対象を、オブジェクト(モノ)に見立てて、オブジェクトを中心としてコードを組み立てていく手法のことを「オブジェクト指向」と呼びます。
オブジェクト指向言語として有名なのは、JavaやC#やRubyですが、JavaScriptもオブジェクト指向言語です。
現実世界のモノ・ことをオブジェクトとして扱い、そのままクラスで表現することは、これからたくさん学習します。
まずは、JavaScriptがオブジェクト指向言語であることを、すこし学んでみましょう。
JavaScriptの継承
オブジェクト指向言語を理解するうえで、重要な概念の1つが継承です。
継承とは、元になるオブジェクト(クラス)の機能を引き継いで、新たなオブジェクト(クラス)を定義する機能です。
Object.createメソッドを用いる継承
Object.createメソッドは、引数にオブジェクトを渡すと、元オブジェクトに参照元を指定した新しいオブジェクトを作成します。
継承しただけでは、空っぽのオブジェクトに見えますが、プロパティを確認すると継承元を参照していることがわかります。
function myFunction1_02_00() {
const animal = { genre: 'cat' };
//animalを継承する
const kitty = Object.create(animal);
console.log(kitty); //{}
console.log(kitty.genre); //cat
//kittyにプロパティを追加する
kitty.name = 'kitty';
console.log(kitty); //{ name: 'kitty' }
console.log(kitty.genre, kitty.name); //cat kitty
//参照元を変更すると継承先に影響を及ぼす
animal.genre = 'dog';
console.log(kitty.genre); //dog
}
class構文
ES6から導入されたクラス構文では、継承をスッキリ書けるようになりました。
function myFunction1_02_01() {
/** スーパークラスAnimal */
class Animal {
/**
* 動物名を受け取るコンストラクタ
* @param {string} string 動物名
*/
constructor(name) {
this.name = name;
}
/**
* 挨拶を返すメソッド
* @return {string} 挨拶文
*/
greet() { return `Hello, My ${this.name}`; }
}
/**
* サブクラスCat
* @extends Animal
*/
class Cat extends Animal {
/**
* 鳴くメソッド
* @return {string} 鳴き声
*/
meow() { return 'meow meow!'; }
/**
* 名を名乗るメソッド
* @return {string} 挨拶文
*/
greet() { return `I'm ${this.name}`; } //メソッドのオーバーライド
}
const c = new Cat('oliver');
console.log(c.name); //oliver
console.log(c.meow()); //meow meow!
console.log(c.greet()); //I'm oliver
}
JavaScriptの継承は、継承元を参照するプロトタイプという仕組みで実装されています。
プロトタイプについては、別途ブログを書きます。
継承の3タイプ
継承をする理由は「再利用」です。一度作ったクラスを、別の場所で再利用するのに便利です。
クラスの再利用には、3つの考え方があります。
- 一般化/特殊化
- 共通部分の抽出
- 差分実装
それぞれ見ていきましょう。
一般化/特殊化
1つ目は、「スーパークラスで一般的な機能を実装し、サブクラスで特殊な機能を実装する」 というタイプの継承です。
このタイプは、クラスを「分類」という使い方にしたと言えるでしょう。
メソッドは新たに加えられることもありますし、オーバーライドされることもあります。
ソースコードは、先ほどと同じです。
function myFunction1_02_01() {
/** スーパークラスAnimal */
class Animal {
/**
* 動物名を受け取るコンストラクタ
* @param {string} string 動物名
*/
constructor(name) {
this.name = name;
}
/**
* 挨拶を返すメソッド
* @return {string} 挨拶文
*/
greet() { return `Hello, My ${this.name}`; }
}
/**
* サブクラスCat
* @extends Animal
*/
class Cat extends Animal {
/**
* 動物名を受け取るコンストラクタ
* @param {string} string 動物名
*/
constructor(name) {
super(name);
}
/**
* 鳴くメソッド
* @return {string} 鳴き声
*/
meow() { return 'meow meow!'; }
/**
* 名を名乗るメソッド
* @return {string} 挨拶文
*/
greet() { return `I'm ${this.name}`; } //メソッドのオーバーライド
}
const c = new Cat('oliver');
console.log(c.name); //oliver
console.log(c.meow()); //meow meow!
console.log(c.greet()); //I'm oliver
}
共通部分の抽出
2つ目は、「複数のクラスの共通部分を、スーパークラスで抽出する」 というタイプの継承です。
このタイプは、クラスを「共通処理のパーツ化」という使い方にしたと言えるでしょう。
ソースコードではこのようになるでしょう。
//[再利用前のコード]
function myFunction1_02_02() {
/** クラス */
class Shironekoyamato {
/**
* 営業所名を受け取るコンストラクタ
* @param {string} string 営業所
*/
constructor(branchName) {
this.branchName = branchName;
}
/**
* 不在表を投函するメソッド
* @return {string} 挨拶文
*/
postFuzaihy() { return `留守だったようで不在表を投函します`; }
/**
* 挨拶を返すメソッド
* @return {string} 挨拶文
*/
greet() { return `シロネコヤマト${this.branchName}店です!`; }
}
/** クラス */
class Sayamakyuubin {
/**
* 営業所名を受け取るコンストラクタ
* @param {string} string 営業所
*/
constructor(branchName) {
this.branchName = branchName;
}
/**
* 不在表を投函するメソッド
* @return {string} 挨拶文
*/
postFuzaihy() { return `留守だったようで不在表を投函します`; }
/**
* 挨拶を返すメソッド
* @return {string} 挨拶文
*/
greet() { return `佐山急便${this.branchName}店です!`; }
}
const sySapporo = new Shironekoyamato('札幌');
console.log(sySapporo.greet()); //シロネコヤマト札幌店です!
const skObihiro = new Sayamakyuubin('帯広');
console.log(skObihiro.greet()); // 佐山急便帯広店です!
}
継承を活用すると、このようになります。
//[再利用後のコード]
function myFunction1_02_03() {
/** スーパークラスHaisogyosya */
class Haisogyosya {
/**
* 営業所名を受け取るコンストラクタ
* @param {string} string 営業所名
*/
constructor(branchName) {
this.branchName = branchName;
}
/**
* 不在表を投函するメソッド
* @return {string} 挨拶文
*/
postFuzaihy() { return `留守だったようで不在表を投函します`; }
}
/**
* サブクラスシロネコヤマト
* @extends Haisogyosya
*/
class Shironekoyamato extends Haisogyosya {
/**
* 挨拶を返すメソッド
* @return {string} 挨拶文
*/
greet() { return `シロネコヤマト${this.branchName}店です!`; }
}
/**
* サブクラス佐山急便
* @extends Haisogyosya
*/
class Sayamakyuubin extends Haisogyosya {
/**
* 挨拶を返すメソッド
* @return {string} 挨拶文
*/
greet() { return `佐山急便${this.branchName}店です!`; }
}
const sySapporo = new Shironekoyamato('札幌');
console.log(sySapporo.greet()); //シロネコヤマト札幌店です!
const skObihiro = new Sayamakyuubin('帯広');
console.log(skObihiro.greet()); // 佐山急便帯広店です!
}
差分実装
3つ目は、「継承して、変更点だけを実装する」 というタイプの継承です。
このタイプは、クラスを「コピーするためのひな形」という使い方にしたと言えるでしょう。
ソースコードではこのようになるでしょう。
function myFunction1_02_04() {
/** スーパークラスSeason */
class Season {
/**
* 警告を出すメソッド
* @return {string} 警告文
*/
alertSpeed() {
return `スピードの出し過ぎに気を付けましょう!`;
}
}
/**
* サブクラス冬
* @extends Season
*/
class Winter extends Season {
/**
* 警告を出すメソッド
* @return {string} 警告文
*/
alertStudlessTires() {
return `スタッドレスタイヤを着用しましょう!`;
}
}
const s = new Season();
console.log(s.alertSpeed()); //スピードの出し過ぎに気を付けましょう!
const w = new Winter();
console.log(w.alertSpeed()); //スピードの出し過ぎに気を付けましょう!
console.log(w.alertStudlessTires()); //スタッドレスタイヤを着用しましょう!
}
継承のルール(個人的なメモ)
繰り返しますが、継承をする理由は「再利用」です。
しかし、便利になる一方で、読み辛いコードになってしまっては本末転倒です。
そこで、現時点でのルールを3つ設定してみました。
- サブクラスのサブクラスを作らない
- クラスを型や分類として限定しない
- 多重継承をしない
これらは、まだフワッとしている概念なので、別のシリーズで、1つ1つ意味を解説していきたいと思います。
そして、その頃にはこのルールも変わっている可能性があります。
まとめ
以上で、「クラスと継承」をお届けしました。
継承は、クラスの技術的なテクニックです。
しかし、継承の「分類」や「処理の共通化」や「差分実装」という3つのタイプを眺めると、現実世界をオブジェクトとして表現するヒントがありそうです。
第2章では、引き続きクラスの書き方とオブジェクト指向をお届けします。
参考資料
- 継承とプロトタイプチェーン MDN
- 改定新版JavaScript本格入門~モダンスタイルによる基礎から現場での応用まで
- コーディングを支える技術 ~成り立ちから学ぶプログラミング作法 (WEB+DB PRESS plus)
このシリーズの目次
Google Apps Scriptとドメイン駆動設計