2019年3月7日
JavaScriptとオブジェクト指向プログラミング
(2018-09-24)by RAINER HAHNEKAMP
本記事は、原著者の許諾のもとに翻訳・掲載しております。
本稿は、オブジェクト指向プログラミング(OOP)について予備知識のないJavaScriptの学習者向けに書かれています。OOP の中でJavaScriptに関連する部分にのみ焦点を当て、OOPの概要については説明しません。ポリモーフィズムについては、静的型付け言語の方が適しているため省きます。
なぜOOPを知る必要があるか?
あなたは初めてのプログラム言語にJavaScriptを選びましたか? あなたはコードが10万行以上にわたる巨大企業のシステムを扱う腕利きの開発者になりたいですか?
オブジェクト指向プログラミングを最大限活用できるように学ばなければ、到底無理でしょう。
様々な考え方
サッカーでは、安全に守りを固めることもできますし、サイドからの高いボールに飛びつくこともできます。また、先など考えずに攻撃することも可能です。これらの戦略は全て同じ目的を持っています。それは試合に勝つことです。
同じことがプログラミングパラダイムにも当てはまります。問題へのアプローチやソリューションの設計に様々な方法で取り組めます。
オブジェクト指向プログラミング、つまりOOPは、最新のアプリケーション開発のためのパラダイムであり、Java、C#、JavaScriptなどの主要言語によってサポートされています。
オブジェクト指向パラダイム
OOPの観点で考えると、アプリケーションは相互に作用する”オブジェクト”の集まりです。これらのオブジェクトは、製品の在庫や従業員の記録など現実世界の情報に準じます。オブジェクトはデータを持ち、そのデータに基づいて何かしらのロジックを実行するのです。その結果、OOPのコードはとても理解しやすくなります。あまり簡単ではないのは、最初にアプリケーションを小さなオブジェクトに分ける方法を決めることです。
この話を初めて聞いた時の私と同じなら、この本当の意味は分からないでしょう。全てがとても抽象的に思えます。このように感じることは全く問題ありません。大切なのは、この概念を耳にしたことがあり、記憶に留め、プログラミングでOOPを使おうとすることです。時間とともに経験を積み、この理論的な概念に合うコードを書くことが増えるでしょう。
学んだこと: 現実世界のオブジェクトに基づくOOPでは、誰でもコードを読むことができ、どうなっているかを理解できる。
中心としてのオブジェクト
中心としてのオブジェクト
どのようにJavaScriptがOOPの基本原理を実装するかを知るには、簡単な例が役立つでしょう。製品を買い物カゴに入れ、支払う合計金額を計算するというショッピングのユースケースを考えます。OOPを使わずにJavaScriptの知識を用いて、このユースケースをプログラミングすると次のようになります。
const bread = {name: 'Bread', price: 1};
const water = {name: 'Water', price: 0.25};
const basket = [];
basket.push(bread);
basket.push(bread);
basket.push(water);
basket.push(water);
basket.push(water);
const total = basket
.map(product => product.price)
.reduce((a, b) => a + b, 0);
console.log('one has to pay in total: ' + total);
OOPの考え方なら、もっと簡単に優れたコードを書けます。なぜならオブジェクトを現実世界で遭遇したかのように考えるからです。ユースケースにはバスケットに入った製品があるので、既に2種類のオブジェクト、つまりバスケットのオブジェクトと製品のオブジェクトがあります。
OOPバージョンでは、ショッピングのユースケースは次のように書けるでしょう。
const bread = new Product("bread", 1);
const water = new Product("water", .25);
const basket = new Basket();
basket.addProduct(2, bread);
basket.addProduct(3, water);
basket.printShoppingInfo();
1行目を見ると分かりますが、いわゆるクラスと呼ばれるコード(後ほど説明します)の名前の前に new
というキーワードを使い、新しいオブジェクトを作っています。これで、格納したオブジェクトを変数breadに返します。変数waterにも同じことを繰り返し、似たような方法で変数basketを作ります。これらの製品を買い物かごに加えると、最後に支払う合計金額が出力されます。
学んだこと: 現実世界のものをモデルにしたオブジェクトはデータと関数で構成されている。
テンプレートとしてのクラス
テンプレートとしてのクラス
OOPでは、オブジェクトを作るためのテンプレートとしてクラスを使います。オブジェクトは”クラスのインスタンス”で、”インスタンス化”はクラスに基づくオブジェクトを作ることです。コードはクラスによって定義されますが、生存中のオブジェクト内でなければ実行できません。
クラスは自動車の設計図のようなものです。設計図は、トルクや馬力といったプロパティ、空燃比などの内部関数、点火装置のように誰もがアクセスできるメソッドを定義します。しかし、鍵を回せば車が走るのは、工場で車をインスタンス化している時だけです。
今回のユースケースではProductクラスを使って、breadとwaterという2つのオブジェクトをインスタンス化します。もちろん、これらのオブジェクトで必要とするコードはクラスで与えなければいけません。次のようになります。
function Product(_name, _price) {
const name = _name;
const price = _price;
this.getName = function() {
return name;
};
this.getPrice = function() {
return price;
};
}
function Basket() {
const products = [];
this.addProduct = function(amount, product) {
products.push(...Array(amount).fill(product));
};
this.calcTotal = function() {
return products
.map(product => product.getPrice())
.reduce((a, b) => a + b, 0);
};
this.printShoppingInfo = function() {
console.log('one has to pay in total: ' + this.calcTotal());
};
}
JavaScriptのクラスは関数のように見えますが、使い方は異なります。関数名はクラス名で大文字になっています。これは何も返さないので、 const basket = Product("bread", 1);
というような通常の方法で関数を呼び出しません。その代り、 const basket = new Product("bread", 1);
のように、newというキーワードを加えます。
関数内のコードがコンストラクターで、これはオブジェクトがインスタンス化されるたびに実行されます。Productが持つパラメータは _name
と _price
です。新しいオブジェクトは、それぞれの内部にこれらの値を格納します。
更に、オブジェクトが提供する関数を定義できます。外部からのアクセスを可能にする this
というキーワードを頭に加えることで関数を定義します(カプセル化の項をご覧ください)。関数がプロパティに自由にアクセスできることに留意してください。
クラスBasketは、新しいオブジェクトを作る際に、いかなる引数も必要としません。新しいBasketオブジェクトをインスタンス化すると、単純に、後でプログラムが埋められる空のproductsリストを生成します。
学んだこと: クラスは、実行中にオブジェクトを生成するテンプレートである。
カプセル化
クラスの宣言の別のやり方を見かけるかもしれません。
function Product(name, price) {
this.name = name;
this.price = price;
}
変数 this
へのプロパティの割り当てを思い出してください。ひと目で、これがより良い方法だと分かるでしょう。getter (getName & getPrice)というメソッドはもう必要ないので、よりすっきりしています。
残念なことに、外部からプロパティに自由にアクセスできるようになってしまいました。ですから、誰もがプロパティにアクセスし、変更することが可能です。
const bread = new Product('bread', 1);
bread.price = -10;
このようにすると、アプリケーションのメンテナンスがさらに難しくなるため、これはやりたくないと考えるでしょう。例えば、priceがマイナスにならないように、バリデーションコードを追加したら、何が起きるでしょうか。priceのプロパティに直接アクセスするコードは全て、バリデーションを回避します。これは、追跡困難なエラーを引き起こす可能性があります。それに対し、オブジェクトのgetterメソッドを使うコードは、必ずオブジェクトのpriceバリデーションを通ります。
オブジェクトはそのデータに対して排他的制御を実行する必要があります。言い換えれば、オブジェクトはそのデータを”カプセル化”し、他のオブジェクトが直接データにアクセスすることを防ぐ必要があります。オブジェクト内部に書かれた関数を経由して間接的にアクセスするのが、データにアクセスする唯一の方法です。
データとプロセスは(別名ロジックともいいますが)、切り離せないものです。処理データが明確に定義されている場所に限られていることが重要であるような大規模なアプリケーションの場合、特にその傾向が強くなります。
正しく扱えば、結果OPPは設計によってモジュール性を生じます。これはソフトウェア開発の分野における究極の目標です。こうして、全てが密接に連動していて、コードを少し変更しただけで何が起こるか判らないような、酷いスパゲティプログラムになることを回避できます。
今回の場合、クラスProductのオブジェクトは初期化後にpriceや名nameを変更することはできません。Productのインスタンスは読み込み専用です。
学んだこと: カプセル化により、オブジェクトの関数を経由しないでデータにアクセスするのを防ぐ。
継承
継承を使用すれば、追加のプロパティと関数を用いて既に存在しているクラスを拡張することにより、新しいクラスを作成できます。新しいクラスは、親の特性をすべて”継承”しているので、新しいコードをゼロから作らなくて済みます。さらに、親クラスで行われた変更はすべて、自動的に子クラスでも使用できるので、アップデートがより簡単に行えます。
nameとpriceとauthorの要素を持つBookと呼ばれる新しいクラスがあると仮定します。継承により、BookはProductと同じだと言うことができますが、さらにauthorというプロパティも持っています。ProductはBookのスーパークラス、BookはProductのサブクラスということになります。
function Book(_name, _price, _author) {
Product.call(this, _name, _price);
const author = _author;
this.getAuthor = function() {
return author;
}
}
注目すべきは第1引数として this
を伴う追加の Product.call
です。また以下のことを知っておいてください。bookはgetterメソッドを提供しますが、まだnameとpriceのプロパティに直接アクセスはできません。bookはProductクラスからそのデータを呼び出す必要があります。
これで、何の問題もなく、Basketにbookオブジェクトを追加することができます。
const faust = new Book('faust', 12.5, 'Goethe');
basket.addProduct(1, faust);
BasketはProduct型のオブジェクトを期待しています。またBookを経由してProductから継承しているので、bookはProductです。
学んだこと: サブクラスは、スーパークラスのプロパティと関数を継承することができる。さらに独自にプロパティや関数を追加することが可能だ。
JavaScriptとOOP
JavaScriptアプリケーションを作成するのに使用されているプログラミングパラダイムが3種類あることに気付くでしょう。この3つとはプロトタイプベースプログラミング、オブジェクト指向プログラミング、関数指向プログラミングです。
この理由は、JavaScriptの歴史にあります。そもそもJavaScriptはプロトタイプベースでした。大規模なアプリケーションに使用するような言語ではなかったのです。
設計者の意図に反して、デベロッパたちは、次第により大きなアプリケーションにJavaScriptを用いるようになりました。OOPは、もともとのプロトタイプベースプログラミングの技術の上に結合されたものなのです。
プロトタイプベースのアプローチは下に示しますが、クラスを構成するために”古典的かつ既定の方法”として考えられています。残念ながら、カプセル化はサポートしていません。
OOPに対するJavaScriptのサポートは、Javaのような他の言語と同レベルではないのですが、今もまだ進化し続けています。ES6版のリリースでは、欲しいと思っていた専用の class
キーワードが追加されました。内部的には、プロトタイプのプロパティと同じ目的で用いられましたが、コードのサイズが小さくなります。しかし、ES6のクラスにはまだプライベートなプロパティが欠けています。そのため、”古いやり方”から抜け出せないのです。
完全を期すために、ES6のクラスを用いて、Product、Basket、Bookをどのように書くか、またプロトタイプ(古典的かつ既定)なアプローチを用いた場合についても下記に示します。これらのバージョンはカプセル化を提供していないことに気を付けてください。
// ES6 version
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
class Book extends Product {
constructor(name, price, author) {
super(name, price);
this.author = author;
}
}
class Basket {
constructor() {
this.products = [];
}
addProduct(amount, product) {
this.products.push(...Array(amount).fill(product));
}
calcTotal() {
return this.products
.map(product => product.price)
.reduce((a, b) => a + b, 0);
}
printShoppingInfo() {
console.log('one has to pay in total: ' + this.calcTotal());
}
}
const bread = new Product('bread', 1);
const water = new Product('water', 0.25);
const faust = new Book('faust', 12.5, 'Goethe');
const basket = new Basket();
basket.addProduct(2, bread);
basket.addProduct(3, water);
basket.addProduct(1, faust);
basket.printShoppingInfo();
//Prototype version
function Product(name, price) {
this.name = name;
this.price = price;
}
function Book(name, price, author) {
Product.call(this, name, price);
this.author = author;
}
Book.prototype = Object.create(Product.prototype);
Book.prototype.constructor = Book;
function Basket() {
this.products = [];
}
Basket.prototype.addProduct = function(amount, product) {
this.products.push(...Array(amount).fill(product));
};
Basket.prototype.calcTotal = function() {
return this.products
.map(product => product.price)
.reduce((a, b) => a + b, 0);
};
Basket.prototype.printShoppingInfo = function() {
console.log('one has to pay in total: ' + this.calcTotal());
};
学んだこと: OOPは、開発後期にJavaScriptに追加された。
まとめ
JavaScriptを学んでいる新しいプログラマとして、オブジェクト指向プログラミングを完全に理解するには時間がかかります。初期の段階で、理解しなければならない重要なことは、OOPのパラダイムが基にしている原則と、もたらされる利点です。
- 実在のものをモデルにしているオブジェクトは、あらゆるOOPを基にしたアプリケーションの中心である。
- カプセル化は自由なアクセスからデータを保護する。
- オブジェクトは、オブジェクトに含まれるデータを操作する関数を有している。
- クラスはオブジェクトのインスタンスを作成するために使用されるテンプレートである。
- 継承は冗長性を避けるための強力なツールである。
- OOPは冗長だが、他のコーディングパラダイムに比べて読みやすい。
- OOPは開発の後期に追加されたので、プロトタイプや関数指向プログラミングの技術を使った古いコードを目にすることがある。
参考文献
- https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_JS
- http://voidcanvas.com/es6-private-variables/
- https://medium.com/@rajaraodv/is-class-in-es6-the-new-bad-part-6c4e6fe1ee65
- https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Inheritance
- https://en.wikipedia.org/wiki/Object-oriented_programming
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa